Quickbooks: Why each Empire Ring Member needs their own Accounting and Banking

Quickbooks: Why each Empire Ring Member needs their own Accounting and Banking
Note... articles to be later expanded into separate forum posts.
INDEX OF ARTICLES

Awesome—here’s a tight, end-to-end content plan for your forum PLUS a ready-to-drop .NET 8 QuickBooks Online (QBO) integration starter that demonstrates OAuth, REST calls (create customer, invoice, payment), and webhooks. You’ll get: 1) article list (with slugs and goals), 2) banking/accounting structure policy posts, and 3) full code regeneration with filenames + commented code.


Forum Series Outline (slugs, goals, and who it’s for)​


  1. quickbooks/why-each-man-needs-his-own-books
    Goal: Make the case for every member running their own QuickBooks Online (QBO) tenant for sovereignty, credit-building, and clean audit trails.
  2. quickbooks/each-llc-its-own-qbo-and-two-bank-accounts
    Goal: Policy: each LLC = separate QBO + two bank accounts (Operating + Tax/Reserve). How this prevents co-mingling and simplifies audits.
  3. quickbooks/choose-right-qbo-plan-for-international-man
    Goal: Simple matrix of QBO plans and when to upgrade (multi-currency, inventory, projects, 1099s).
  4. quickbooks/chart-of-accounts-template-for-one-man-llc
    Goal: Downloadable COA template aligned to your Transaction Equity model (Income, COGS, Operating, Tax/Holdback).
  5. quickbooks/banking-setup-two-accounts-rule
    Goal: Bank checklist: create Operating + Tax/Reserve, set online banking, enable bank feeds, set bank-rules to auto-sort deposits/expenses.
  6. quickbooks/cash-vs-accrual-and-when-to-switch
    Goal: Explain Cash vs Accrual in plain English, with examples for trades, media, consulting.
  7. quickbooks/sales-tax-basics-and-when-to-register
    Goal: Who needs sales tax, multi-state exposure, digital products vs services, link to state portals.
  8. quickbooks/contractors-1099-w9-policies
    Goal: W-9 intake SOP, 1099-NEC at year end, document storage policy.
  9. quickbooks/multi-currency-for-international-ops
    Goal: Turn on multi-currency, FX impact on P&L, clean memo usage.
  10. quickbooks/receipts-inbox-mastery
    Goal: Email-to-QBO, mobile app capture, naming standards, monthly close checklist.
  11. quickbooks/monthly-close-checklist-7-steps
    Goal: Bank recs, undeposited funds cleanup, A/R & A/P aging, sales tax payable, owner draw log.
  12. quickbooks/owner-pay-yourself-legally
    Goal: LLC draws vs payroll (when S-Corp), documenting distributions, don’t co-mingle.
  13. quickbooks/llc-to-llc-intercompany-invoices
    Goal: Clean intercompany SOP for your group: memo standards, pricing policy, audit trail.
  14. quickbooks/api-overview-what-we-can-automate
    Goal: High-level QBO API objects: Customer, Item, Invoice, Payment, Vendor, Bill, JournalEntry, Webhooks.
  15. quickbooks/oauth2-deep-dive-keys-and-callbacks
    Goal: How OAuth2 works, setting Redirect URIs, refresh tokens, storing realmId/companyId.
  16. quickbooks/rest-api-create-customer-invoice-payment
    Goal: Tutorial + code: create Customer → Invoice → record Payment (full example below).
  17. quickbooks/webhooks-architecture-events-security
    Goal: Subscribing to QBO Webhooks, validating signatures, idempotency, retry strategy.
  18. quickbooks/bank-feeds-rules-and-auto-categorization
    Goal: Bank rules for common vendors, merchant regex tricks, fuel receipts, per-diem tagging.
  19. quickbooks/inventory-light-vs-full-and-items-setup
    Goal: When to use Items, mapping to revenue accounts, COGS basics.
  20. quickbooks/project-and-class-tracking-for-job-profit
    Goal: Use Projects/Classes to see job profitability; reporting pack for weekly standups.
  21. quickbooks/tax-reserve-policy-10-to-30-percent
    Goal: How much to sweep into Tax/Reserve weekly; automate transfers; never touch the pot.
  22. quickbooks/merchant-processing-surcharges-and-fees
    Goal: Record Stripe/PayPal fees properly; gross vs net deposits; Undeposited Funds workflow.
  23. quickbooks/expense-management-per-diem-and-mileage
    Goal: Mileage apps, per-diem policy, reimbursable vs owner draw.
  24. quickbooks/ai-bookkeeping-workbench
    Goal: Use your nodes + Python to summarize uncategorized transactions, suggest categories, and post via API.
  25. quickbooks/close-the-books-year-end
    Goal: Year-end close SOP, archive bank statements, CPA package, 1099s, W-2s (if S-Corp).
  26. quickbooks/security-and-access-control-per-member
    Goal: Principle of Least Privilege; accountant vs standard user; revoke on exit.
  27. quickbooks/mistakes-to-avoid-new-llc
    Goal: Top 20 errors: co-mingling, no receipts, wrong sales tax, invoices without terms, etc.
  28. quickbooks/two-account-playbook-case-studies
    Goal: Before/after examples of clarity from two-account policy.
  29. quickbooks/vendor-bill-automation-and-approvals
    Goal: Bills vs Expenses, approval chains, due-date discipline, cash-flow forecast.
  30. quickbooks/your-first-automation-roadmap
    Goal: Start here: OAuth → Create Customer → Invoice → Payment → Webhooks → Close checklist.

Banking + Entity Structure (policy posts)​


• Policy: One human = 1 personal QBO (optional) + 1+ business QBO(s) for each LLC they operate.
• Policy: Every LLC must have its own QBO tenant and exactly two bank accounts minimum: Operating and Tax/Reserve (add Savings/CapEx later).
• Policy: No co-mingling. No shared cards across entities. Document every transfer with memo (reason, date, parties).
• SOP: Weekly sweep to Tax/Reserve per Revenue % band (10–30% guideline).
• SOP: Intercompany billing via formal invoices; never “mystery transfers.”
• SOP: Close Day (monthly): reconcile banks, lock prior periods, export backup.


Full Code Regeneration — QuickBooks REST Starter (ASP.NET Core 8, C#)​


Solution name: QuickBooksGateway
Purpose: OAuth2 with Intuit, store tokens, call REST to create Customer/Invoice/Payment, receive Webhooks.


FILES:


  1. QuickBooksGateway/QuickBooksGateway.csproj
  2. QuickBooksGateway/appsettings.json
  3. QuickBooksGateway/Program.cs
  4. QuickBooksGateway/Models/QboOptions.cs
  5. QuickBooksGateway/Models/QboDtos.cs
  6. QuickBooksGateway/Data/TokenDbContext.cs
  7. QuickBooksGateway/Entities/QboToken.cs
  8. QuickBooksGateway/Services/ITokenStore.cs
  9. QuickBooksGateway/Services/TokenStore.cs
  10. QuickBooksGateway/Services/QboClient.cs
  11. QuickBooksGateway/Controllers/QboAuthController.cs
  12. QuickBooksGateway/Controllers/QboApiController.cs
  13. QuickBooksGateway/Controllers/WebhooksController.cs

CODE (paste each file exactly as named):


— QuickBooksGateway/QuickBooksGateway.csproj —


net8.0
enable
enable
true




all


all



— QuickBooksGateway/appsettings.json —
{
"Qbo": {
"ClientId": "YOUR_INTUIT_CLIENT_ID",
"ClientSecret": "YOUR_INTUIT_CLIENT_SECRET",
"RedirectUri": "https://yourdomain.com/qbo/callback",
"Environment": "sandbox",
"Scopes": "com.intuit.quickbooks.accounting openid profile email phone address",
"BaseAuthUrl": "https://appcenter.intuit.com/connect/oauth2",
"TokenUrl": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
"ApiBaseUrlSandbox": "https://sandbox-quickbooks.api.intuit.com/v3/company/",
"ApiBaseUrlProd": "https://quickbooks.api.intuit.com/v3/company/"
},
"ConnectionStrings": {
"Default": "Server=localhost;Database=QboGateway;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
},
"WebhookVerifier": {
"IntuitSignatureHeader": "intuit-signature",
"VerifierToken": "YOUR_WEBHOOK_VERIFIER_TOKEN"
},
"AllowedHosts": "*"
}


— QuickBooksGateway/Program.cs —
using Microsoft.EntityFrameworkCore;
using QuickBooksGateway.Data;
using QuickBooksGateway.Models;
using QuickBooksGateway.Services;


var builder = WebApplication.CreateBuilder(args);


// Bind QBO options
builder.Services.Configure(builder.Configuration.GetSection("Qbo"));


// EF Core
builder.Services.AddDbContext(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));


// Services
builder.Services.AddHttpClient();
builder.Services.AddScoped<ITokenStore, TokenStore>();
builder.Services.AddScoped();


builder.Services.AddControllers().AddNewtonsoftJson();


var app = builder.Build();


app.MapControllers();


app.Run();


— QuickBooksGateway/Models/QboOptions.cs —
namespace QuickBooksGateway.Models
{
// Holds configuration needed for OAuth + API calls
public class QboOptions
{
public string ClientId { get; set; } = "";
public string ClientSecret { get; set; } = "";
public string RedirectUri { get; set; } = "";
public string Environment { get; set; } = "sandbox";
public string Scopes { get; set; } = "";
public string BaseAuthUrl { get; set; } = "";
public string TokenUrl { get; set; } = "";
public string ApiBaseUrlSandbox { get; set; } = "";
public string ApiBaseUrlProd { get; set; } = "";


public string ApiBaseUrl(string env) =&gt;<br> env?.ToLower() == "prod" ? ApiBaseUrlProd : ApiBaseUrlSandbox;<br>}<br>

}


— QuickBooksGateway/Models/QboDtos.cs —
using System.Text.Json.Serialization;


namespace QuickBooksGateway.Models
{
// Minimal DTOs for QBO create flows (Customer, Invoice, Payment).
// QBO is picky about casing and required fields—keep payloads aligned with API docs.


public record CreateCustomerRequest(string DisplayName, string? PrimaryEmailAddr);<br><br>public record CreateInvoiceRequest(long CustomerId, decimal Amount, string? Memo);<br><br>public record RecordPaymentRequest(long CustomerId, long InvoiceId, decimal Amount);<br><br>// Basic API responses<br>public class QboCustomerResponse<br>{<br> [JsonPropertyName("Customer")]<br> public QboCustomer? Customer { get; set; }<br>}<br><br>public class QboCustomer<br>{<br> public long Id { get; set; }<br> public string? DisplayName { get; set; }<br> public string? SyncToken { get; set; }<br>}<br><br>public class QboInvoiceResponse<br>{<br> [JsonPropertyName("Invoice")]<br> public QboInvoice? Invoice { get; set; }<br>}<br><br>public class QboInvoice<br>{<br> public long Id { get; set; }<br> public string? SyncToken { get; set; }<br> public long? CustomerRef { get; set; }<br> public decimal? TotalAmt { get; set; }<br>}<br><br>public class QboPaymentResponse<br>{<br> [JsonPropertyName("Payment")]<br> public QboPayment? Payment { get; set; }<br>}<br><br>public class QboPayment<br>{<br> public long Id { get; set; }<br> public string? SyncToken { get; set; }<br> public decimal? TotalAmt { get; set; }<br>}<br>

}


— QuickBooksGateway/Data/TokenDbContext.cs —
using Microsoft.EntityFrameworkCore;
using QuickBooksGateway.Entities;


namespace QuickBooksGateway.Data
{
// Stores OAuth tokens per connected QBO company (realmId)
public class TokenDbContext : DbContext
{
public TokenDbContext(DbContextOptions options) : base(options) { }


public DbSet&lt;QboToken&gt; Tokens =&gt; Set&lt;QboToken&gt;();<br><br> protected override void OnModelCreating(ModelBuilder modelBuilder)<br> {<br> modelBuilder.Entity&lt;QboToken&gt;(e =&gt;<br> {<br> e.HasKey(x =&gt; x.Id);<br> e.HasIndex(x =&gt; x.RealmId).IsUnique();<br> e.Property(x =&gt; x.AccessToken).IsRequired();<br> e.Property(x =&gt; x.RefreshToken).IsRequired();<br> e.Property(x =&gt; x.ExpiresAtUtc);<br> });<br> }<br>}<br>

}


— QuickBooksGateway/Entities/QboToken.cs —
using System.ComponentModel.DataAnnotations;


namespace QuickBooksGateway.Entities
{
// One row per QuickBooks company (realmId)
public class QboToken
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();


// Intuit company identifier (realm/company id)<br> public string RealmId { get; set; } = "";<br><br> public string AccessToken { get; set; } = "";<br> public string RefreshToken { get; set; } = "";<br><br> // When current access token expires (UTC)<br> public DateTime ExpiresAtUtc { get; set; }<br><br> // Sandbox or prod so we can route API base URL<br> public string Environment { get; set; } = "sandbox";<br><br> // For multi-member systems: who connected the company<br> public string? ConnectedByUserId { get; set; }<br>}<br>

}


— QuickBooksGateway/Services/ITokenStore.cs —
using QuickBooksGateway.Entities;


namespace QuickBooksGateway.Services
{
public interface ITokenStore
{
Task<QboToken?> GetByRealmIdAsync(string realmId, CancellationToken ct = default);
Task UpsertAsync(QboToken token, CancellationToken ct = default);
}
}


— QuickBooksGateway/Services/TokenStore.cs —
using Microsoft.EntityFrameworkCore;
using QuickBooksGateway.Data;
using QuickBooksGateway.Entities;


namespace QuickBooksGateway.Services
{
// EF Core token persistence
public class TokenStore : ITokenStore
{
private readonly TokenDbContext _db;


public TokenStore(TokenDbContext db) =&gt; _db = db;<br><br> public async Task&lt;QboToken?&gt; GetByRealmIdAsync(string realmId, CancellationToken ct = default)<br> {<br> return await _db.Tokens.AsNoTracking().FirstOrDefaultAsync(x =&gt; x.RealmId == realmId, ct);<br> }<br><br> public async Task UpsertAsync(QboToken token, CancellationToken ct = default)<br> {<br> var existing = await _db.Tokens.FirstOrDefaultAsync(x =&gt; x.RealmId == token.RealmId, ct);<br> if (existing is null)<br> {<br> _db.Tokens.Add(token);<br> }<br> else<br> {<br> existing.AccessToken = token.AccessToken;<br> existing.RefreshToken = token.RefreshToken;<br> existing.ExpiresAtUtc = token.ExpiresAtUtc;<br> existing.Environment = token.Environment;<br> existing.ConnectedByUserId = token.ConnectedByUserId;<br> }<br> await _db.SaveChangesAsync(ct);<br> }<br>}<br>

}


— QuickBooksGateway/Services/QboClient.cs —
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Options;
using QuickBooksGateway.Entities;
using QuickBooksGateway.Models;


namespace QuickBooksGateway.Services
{
// Handles OAuth token refresh + minimal REST calls (Customer, Invoice, Payment).
public class QboClient
{
private readonly IHttpClientFactory _httpFactory;
private readonly IOptions _opts;
private readonly ITokenStore _tokens;


public QboClient(IHttpClientFactory httpFactory, IOptions&lt;QboOptions&gt; opts, ITokenStore tokens)<br> {<br> _httpFactory = httpFactory;<br> _opts = opts;<br> _tokens = tokens;<br> }<br><br> private string ApiBase(string env) =&gt; _opts.Value.ApiBaseUrl(env);<br><br> public string BuildAuthUrl(string state)<br> {<br> var o = _opts.Value;<br> var url =<br> $"{o.BaseAuthUrl}?client_id={Uri.EscapeDataString(o.ClientId)}" +<br> $"&amp;redirect_uri={Uri.EscapeDataString(o.RedirectUri)}" +<br> $"&amp;response_type=code&amp;scope={Uri.EscapeDataString(o.Scopes)}&amp;state={Uri.EscapeDataString(state)}";<br> return url;<br> }<br><br> public async Task&lt;QboToken&gt; ExchangeAuthCodeAsync(string code, string realmId, string environment, CancellationToken ct = default)<br> {<br> var o = _opts.Value;<br> var client = _httpFactory.CreateClient();<br> var req = new HttpRequestMessage(HttpMethod.Post, o.TokenUrl);<br> var basic = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{o.ClientId}:{o.ClientSecret}"));<br> req.Headers.Authorization = new AuthenticationHeaderValue("Basic", basic);<br> req.Content = new FormUrlEncodedContent(new Dictionary&lt;string, string&gt;<br> {<br> ["grant_type"] = "authorization_code",<br> ["code"] = code,<br> ["redirect_uri"] = o.RedirectUri<br> });<br><br> var res = await client.SendAsync(req, ct);<br> res.EnsureSuccessStatusCode();<br> var json = await res.Content.ReadAsStringAsync(ct);<br> using var doc = JsonDocument.Parse(json);<br> var access = doc.RootElement.GetProperty("access_token").GetString()!;<br> var refresh = doc.RootElement.GetProperty("refresh_token").GetString()!;<br> var expiresIn = doc.RootElement.GetProperty("expires_in").GetInt32();<br><br> var token = new QboToken<br> {<br> RealmId = realmId,<br> AccessToken = access,<br> RefreshToken = refresh,<br> ExpiresAtUtc = DateTime.UtcNow.AddSeconds(expiresIn - 60),<br> Environment = environment<br> };<br> await _tokens.UpsertAsync(token, ct);<br> return token;<br> }<br><br> private async Task&lt;QboToken&gt; EnsureAccessAsync(string realmId, CancellationToken ct)<br> {<br> var t = await _tokens.GetByRealmIdAsync(realmId, ct) ?? throw new InvalidOperationException("No token for realmId");<br> if (DateTime.UtcNow &lt; t.ExpiresAtUtc) return t;<br><br> // Refresh access token<br> var o = _opts.Value;<br> var client = _httpFactory.CreateClient();<br> var req = new HttpRequestMessage(HttpMethod.Post, o.TokenUrl);<br> var basic = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{o.ClientId}:{o.ClientSecret}"));<br> req.Headers.Authorization = new AuthenticationHeaderValue("Basic", basic);<br> req.Content = new FormUrlEncodedContent(new Dictionary&lt;string, string&gt;<br> {<br> ["grant_type"] = "refresh_token",<br> ["refresh_token"] = t.RefreshToken<br> });<br> var res = await client.SendAsync(req, ct);<br> res.EnsureSuccessStatusCode();<br> var json = await res.Content.ReadAsStringAsync(ct);<br> using var doc = JsonDocument.Parse(json);<br> t.AccessToken = doc.RootElement.GetProperty("access_token").GetString()!;<br> t.RefreshToken = doc.RootElement.GetProperty("refresh_token").GetString()!;<br> var expiresIn = doc.RootElement.GetProperty("expires_in").GetInt32();<br> t.ExpiresAtUtc = DateTime.UtcNow.AddSeconds(expiresIn - 60);<br><br> await _tokens.UpsertAsync(t, ct);<br> return t;<br> }<br><br> private HttpRequestMessage JsonPost(string url, string token, object payload)<br> {<br> var req = new HttpRequestMessage(HttpMethod.Post, url);<br> req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);<br> req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));<br> req.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");<br> return req;<br> }<br><br> // Create Customer<br> public async Task&lt;QboCustomerResponse?&gt; CreateCustomerAsync(string realmId, string environment, CreateCustomerRequest input, CancellationToken ct)<br> {<br> var t = await EnsureAccessAsync(realmId, ct);<br> var baseUrl = ApiBase(environment) + realmId + "/customer?minorversion=73";<br> var payload = new<br> {<br> DisplayName = input.DisplayName,<br> PrimaryEmailAddr = string.IsNullOrWhiteSpace(input.PrimaryEmailAddr) ? null : new { Address = input.PrimaryEmailAddr }<br> };<br><br> var client = _httpFactory.CreateClient();<br> var req = JsonPost(baseUrl, t.AccessToken, payload);<br> var res = await client.SendAsync(req, ct);<br> res.EnsureSuccessStatusCode();<br> var json = await res.Content.ReadAsStringAsync(ct);<br> return JsonSerializer.Deserialize&lt;QboCustomerResponse&gt;(json);<br> }<br><br> // Create Invoice (single line)<br> public async Task&lt;QboInvoiceResponse?&gt; CreateInvoiceAsync(string realmId, string environment, CreateInvoiceRequest input, CancellationToken ct)<br> {<br> var t = await EnsureAccessAsync(realmId, ct);<br> var baseUrl = ApiBase(environment) + realmId + "/invoice?minorversion=73";<br><br> // NOTE: ItemRef "1" often maps to "Services" in sandbox; adjust for production Items.<br> var payload = new<br> {<br> Line = new[]<br> {<br> new {<br> DetailType = "SalesItemLineDetail",<br> Amount = input.Amount,<br> SalesItemLineDetail = new { ItemRef = new { value = "1" } }<br> }<br> },<br> CustomerRef = new { value = input.CustomerId.ToString() },<br> PrivateNote = input.Memo<br> };<br><br> var client = _httpFactory.CreateClient();<br> var req = JsonPost(baseUrl, t.AccessToken, payload);<br> var res = await client.SendAsync(req, ct);<br> res.EnsureSuccessStatusCode();<br> var json = await res.Content.ReadAsStringAsync(ct);<br> return JsonSerializer.Deserialize&lt;QboInvoiceResponse&gt;(json);<br> }<br><br> // Record Payment applied to Invoice<br> public async Task&lt;QboPaymentResponse?&gt; RecordPaymentAsync(string realmId, string environment, RecordPaymentRequest input, CancellationToken ct)<br> {<br> var t = await EnsureAccessAsync(realmId, ct);<br> var baseUrl = ApiBase(environment) + realmId + "/payment?minorversion=73";<br><br> var payload = new<br> {<br> CustomerRef = new { value = input.CustomerId.ToString() },<br> TotalAmt = input.Amount,<br> Line = new[]<br> {<br> new {<br> Amount = input.Amount,<br> LinkedTxn = new[] { new { TxnId = input.InvoiceId.ToString(), TxnType = "Invoice" } }<br> }<br> }<br> };<br><br> var client = _httpFactory.CreateClient();<br> var req = JsonPost(baseUrl, t.AccessToken, payload);<br> var res = await client.SendAsync(req, ct);<br> res.EnsureSuccessStatusCode();<br> var json = await res.Content.ReadAsStringAsync(ct);<br> return JsonSerializer.Deserialize&lt;QboPaymentResponse&gt;(json);<br> }<br>}<br>

}


— QuickBooksGateway/Controllers/QboAuthController.cs —
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using QuickBooksGateway.Models;
using QuickBooksGateway.Services;


namespace QuickBooksGateway.Controllers
{
// Handles the OAuth handshake with Intuit (connect account → callback).
[ApiController]
[Route("qbo")]
public class QboAuthController : ControllerBase
{
private readonly QboClient _qbo;
private readonly IOptions _opts;


public QboAuthController(QboClient qbo, IOptions&lt;QboOptions&gt; opts)<br> {<br> _qbo = qbo;<br> _opts = opts;<br> }<br><br> // Step 1: Redirect user to Intuit consent<br> [HttpGet("connect")]<br> public IActionResult Connect([FromQuery] string? env = "sandbox")<br> {<br> // state ideally includes a CSRF token &amp; the env ("sandbox" or "prod")<br> var state = $"env={env}&amp;nonce={Guid.NewGuid()}";<br> var url = _qbo.BuildAuthUrl(state);<br> return Redirect(url);<br> }<br><br> // Step 2: Intuit calls back with ?code=&amp;state=&amp;realmId=<br> [HttpGet("callback")]<br> public async Task&lt;IActionResult&gt; Callback([FromQuery] string code, [FromQuery] string state, [FromQuery] string realmId, [FromQuery] string? error = null)<br> {<br> if (!string.IsNullOrEmpty(error)) return BadRequest(error);<br><br> var env = "sandbox";<br> var parts = state.Split('&amp;', StringSplitOptions.RemoveEmptyEntries);<br> foreach (var p in parts)<br> {<br> var kv = p.Split('=');<br> if (kv.Length == 2 &amp;&amp; kv[0] == "env") env = kv[1];<br> }<br><br> await _qbo.ExchangeAuthCodeAsync(code, realmId, env, HttpContext.RequestAborted);<br> return Content($"Connected realmId {realmId} in {env} environment. You can now call /api/customer, /api/invoice, /api/payment.");<br> }<br>}<br>

}


— QuickBooksGateway/Controllers/QboApiController.cs —
using Microsoft.AspNetCore.Mvc;
using QuickBooksGateway.Models;
using QuickBooksGateway.Services;


namespace QuickBooksGateway.Controllers
{
// Minimal API surface to demonstrate transactional calls.
[ApiController]
[Route("api")]
public class QboApiController : ControllerBase
{
private readonly QboClient _qbo;


public QboApiController(QboClient qbo) =&gt; _qbo = qbo;<br><br> // POST /api/customer?realmId=12345&amp;env=sandbox<br> [HttpPost("customer")]<br> public async Task&lt;IActionResult&gt; CreateCustomer([FromQuery] string realmId, [FromQuery] string env, [FromBody] CreateCustomerRequest req)<br> {<br> var result = await _qbo.CreateCustomerAsync(realmId, env, req, HttpContext.RequestAborted);<br> return Ok(result);<br> }<br><br> // POST /api/invoice?realmId=12345&amp;env=sandbox<br> [HttpPost("invoice")]<br> public async Task&lt;IActionResult&gt; CreateInvoice([FromQuery] string realmId, [FromQuery] string env, [FromBody] CreateInvoiceRequest req)<br> {<br> var result = await _qbo.CreateInvoiceAsync(realmId, env, req, HttpContext.RequestAborted);<br> return Ok(result);<br> }<br><br> // POST /api/payment?realmId=12345&amp;env=sandbox<br> [HttpPost("payment")]<br> public async Task&lt;IActionResult&gt; RecordPayment([FromQuery] string realmId, [FromQuery] string env, [FromBody] RecordPaymentRequest req)<br> {<br> var result = await _qbo.RecordPaymentAsync(realmId, env, req, HttpContext.RequestAborted);<br> return Ok(result);<br> }<br>}<br>

}


— QuickBooksGateway/Controllers/WebhooksController.cs —
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json.Linq;


namespace QuickBooksGateway.Controllers
{
// Receives QBO webhooks (e.g., Customer, Invoice changes).
// In Intuit Developer Portal, subscribe to entities and set this endpoint.
[ApiController]
[Route("webhooks/qbo")]
public class WebhooksController : ControllerBase
{
private readonly IConfiguration _cfg;


public WebhooksController(IConfiguration cfg) =&gt; _cfg = cfg;<br><br> [HttpPost]<br> public async Task&lt;IActionResult&gt; Receive()<br> {<br> // Verify signature (HMACSHA256 over raw body using your "VerifierToken").<br> var body = await new StreamReader(Request.Body).ReadToEndAsync();<br> if (!Request.Headers.TryGetValue(_cfg["WebhookVerifier:IntuitSignatureHeader"] ?? "intuit-signature", out StringValues sigs))<br> return Unauthorized("Missing signature");<br><br> var expected = ComputeHmac(_cfg["WebhookVerifier:VerifierToken"] ?? "", body);<br> if (!sigs.Any(s =&gt; s.Equals(expected, StringComparison.OrdinalIgnoreCase)))<br> return Unauthorized("Bad signature");<br><br> // Parse minimal payload &amp; acknowledge<br> var json = JObject.Parse(body);<br> // TODO: enqueue to your bus for idempotent processing<br> // Example: log entity names + operation types<br> var events = json["eventNotifications"]?.First?["dataChangeEvent"]?["entities"];<br> if (events != null)<br> {<br> foreach (var e in events)<br> {<br> var name = (string?)e["name"] ?? "?";<br> var op = (string?)e["operation"] ?? "?";<br> Console.WriteLine($"Webhook: {name} {op}");<br> }<br> }<br><br> return Ok();<br> }<br><br> private static string ComputeHmac(string secret, string data)<br> {<br> using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));<br> var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));<br> return Convert.ToBase64String(hash);<br> }<br>}<br>

}


How this supports your policy (each member & each LLC)​


• Each member connects their own QBO tenant via GET /qbo/connect → approves in Intuit → we store tokens by realmId.
• Each LLC has its own QBO and two bank accounts. Use article #2 as the canonical policy post and link it everywhere.
• Your gateway lets you automate common flows per entity cleanly (no co-mingling), and you can restrict who can call which realmId.
• Webhooks let your nodes react to real accounting events (e.g., invoice paid → trigger Discord/SignalR toast, sweep to Tax/Reserve).


Next steps (drop these as forum “Resources”)​


• Migration: dotnet ef migrations add InitTokens && dotnet ef database update
• In Intuit Developer Portal: create app, set Redirect URI to https://yourdomain.com/qbo/callback, enable Accounting scopes, set Webhook URL to https://yourdomain.com/webhooks/qbo, copy Verifier Token into appsettings.json.
• Postman collection: add POSTs to /api/customer, /api/invoice, /api/payment with realmId & env query params.
• Security hardening: require your own auth on every /api/* route; map realmId to a MemberId (RBAC).
• Extend: add endpoints for Vendor/Bill, Items, BankDeposit, JournalEntry; add background job to refresh tokens nightly; log everything (Serilog).


If you want, I can also generate:
• A QBO Chart of Accounts JSON seed for one-man LLCs.
• A bank-rules CSV starter (typical fuel, hardware, meals, hotels).
• Razor pages + dark theme UI (LLCAIMACHINE styling) to connect and test each member’s realmId.
 
Last edited:
Back
Top