Skip to content

Tax — VAT Calculation, Validation & Compliance

Granit.Tax provides a provider-agnostic tax engine for SaaS and e-commerce. It handles tax rate lookup, online tax ID validation, and EU VAT classification (reverse charge, OSS, export). Two providers ship out of the box: self-hosted EU VAT rules or Stripe Tax API.

  • EU VAT compliance out of the box: reverse charge, OSS, intra-community rules
  • Online validation against VIES (EU), with offline fallback and retry
  • Provider-agnostic — switch between self-hosted and Stripe Tax without code changes
  • Multi-tenant — rates, validations, and metrics are tenant-aware
  • Audit trail — VIES request identifiers stored for ISO 27001 compliance
  • DirectoryGranit.Tax/ Abstractions: ITaxIdValidator, ITaxRateProvider, domain types, metrics
    • Granit.Tax.Internal Self-hosted EU VAT rules + VIES validation + configurable rates
    • Granit.Tax.EntityFrameworkCore DB-persisted rate overrides and validated tax ID cache
    • Granit.Tax.Endpoints Minimal API (validate, rates, rate by country) with RBAC
    • Granit.Tax.Stripe Stripe Tax Calculations + Tax IDs API integration
FeatureInternal (self-hosted)Stripe Tax
EU VAT rulesFull (reverse charge, OSS, export)Stripe-managed
Tax ID validationVIES + offline fallbackStripe Tax IDs
Rate sourceConfig + DB overridesStripe API
US Sales TaxNot supportedSupported
DependencyNone (EU only)Stripe account

The internal provider uses a pure rule engine to classify transactions:

flowchart TD
    START([Transaction]) --> EU{Buyer in EU?}
    EU -- No --> EXPORT[Export 0%]
    EU -- Yes --> SAME{Same country?}
    SAME -- Yes --> DOM[Domestic rate]
    SAME -- No --> VAT{Valid VAT?}
    VAT -- Yes --> RC[Reverse charge 0%]
    VAT -- No --> OSS{OSS enabled?}
    OSS -- Yes --> BUYER[Buyer country rate]
    OSS -- No --> SELLER[Seller country rate]
Program.cs
builder.AddGranitTax(); // Base abstractions + metrics
// Granit.Tax.Internal provides ITaxCalculator, ITaxIdValidator, ITaxRateProvider
// via GranitTaxInternalModule — auto-discovered by the module system.
// Optional: DB-managed rate overrides
builder.AddGranitTaxEntityFrameworkCore(options =>
options.UseNpgsql(connectionString));
appsettings.json
{
"Tax": {
"SellerCountryCode": "BE",
"Stripe": {
"SecretKey": "sk_live_..."
}
}
}

Register GranitTaxStripeModule instead of GranitTaxInternalModule — the abstractions (ITaxCalculator, ITaxIdValidator) are swapped automatically.

{
"Tax": {
"SellerCountryCode": "BE",
"SellerVatNumber": "BE0123456789",
"OssEnabled": true,
"OssRegisteredCountries": ["FR", "DE", "NL"],
"AllowOfflineFallback": true,
"ValidationCacheTtlHours": 24,
"Internal": {
"Rates": {
"StandardRates": { "FI": 0.255 },
"ReducedRates": { "FR": 0.055 }
}
}
}
}
OptionDefaultDescription
SellerCountryCode"BE"Seller’s ISO country code
OssEnabledfalseEnable EU One-Stop Shop for B2C
AllowOfflineFallbacktrueAccept offline VAT check when VIES is down
ValidationCacheTtlHours24FusionCache TTL for VIES results

All endpoints require authorization. Map them with endpoints.MapGranitTax().

MethodPathPermissionDescription
POST/tax/validateTax.Validations.ExecuteValidate a tax ID online
GET/tax/ratesTax.Rates.ReadList all current tax rates
GET/tax/rates/{countryCode}Tax.Rates.ReadGet rate for a country

Meter: Granit.Tax

MetricTypeTagsDescription
granit.tax.calculation.completedCountertenant_id, providerTax calculations completed
granit.tax.validation.completedCountertenant_id, source, is_validTax ID validations completed
granit.tax.vies.requestCountertenant_id, successVIES API requests
  • Invoicing — Invoices consume ITaxCalculator for line-item tax
  • Payments — Payment collection after invoice finalization
  • Subscriptions — Subscription plans trigger invoice creation