Accounting Integration Features and Flows
Developer Resources Accounting integration Accounting Integration Features and Flows Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the accounting integration module.
Source Type Files or Docs What Was Extracted Schema packages/db/src/schema/integration.tsEvery table, enum, and constraint Auth apps/api/src/modules/integration/auth/*.tsAPI key issuance, verification, rate limiting Webhooks apps/api/src/modules/integration/webhooks/*.tsEndpoint lifecycle, dispatch, delivery, retry Feed apps/api/src/modules/integration/feed/integration-feed.service.tsCursor semantics, epoch Restore apps/api/src/modules/integration/restore/integration-restore-reconciler.service.tsWhat a backup restore does to credentials External API apps/api/src/modules/integration/fees-api/**The accounting application's own read/write surface Admin API apps/api/src/modules/integration/clients/**The office's client/key/webhook administration screens Env apps/api/src/config/env.validation.tsINTEGRATION_SECRET_KEYS provisioning
Field Value Module integrationSubmodules clients (admin), auth, feed, keyring, webhooks, restore, fees-api (the accounting application's own surface)Primary user value A school's accounting application reads fee data and reports payments without staff re-entering anything, and the school office can see and control exactly what that application can do. Actors Office/superadmin (client administration), the accounting application (API key holder), the daily sweep/webhook worker Main entry points /api/integration-clients/** (admin), /api/integration/fees/** (the accounting application), QueueName.INTEGRATION_WEBHOOKSMain outputs Issued API keys and webhook secrets (shown once), delivered webhooks, the change feed, payment sync results, mappings Related docs Backend , API . Fee data itself is documented in the Fees module .
Actor Can Do Cannot Do Auth Requirement Notes Office (holding FeeIntegrations_*, superadmin-granted) Register/suspend/revoke a client, issue/revoke its keys, manage its webhooks, read feed status See a key or webhook secret after its one issuing response JWT + RoleGuard + FeeIntegrations_READ/_CREATE/_UPDATE FeeIntegrations is SUPERADMIN_ONLY — never in the automatic admin grantAccounting application Read the fee catalogue/structures/scholarships/charges/students/obligations, report payments, manage its own student/invoice mappings, poll the change feed, receive signed webhooks Write anything but payments and mappings; act as any client but the one its key belongs to; see events private to another client Authorization: Bearer ssk_… + scopeNever a staff JWT — its own authentication layer entirely Webhook receiver (the accounting app's own HTTP endpoint) Receive signed POSTs Trust an unsigned or stale request Verifies X-Skoolsewa-Signature itself The API is the sender, not this actor's authenticator Sweep / worker Deliver webhooks, retry failed deliveries, recover overdue ones, fan events out Anything requiring office judgement Internal IntegrationWebhookDeliveryService, FeeQueueProcessor's webhooks.recover() callSystem (backup restore) Force a new feed epoch, revoke every active key, pause every active webhook — Internal, inside the restore transaction IntegrationRestoreReconciler
Capability Surface Actor Route/Trigger State Written Register a client Admin Office POST /api/integration-clientsintegration_clientsSuspend/activate/revoke Admin Office POST .../{suspend,activate,revoke}integration_clients; revoke cascades keys+webhooksIssue/revoke a key Admin Office POST .../keys, .../keys/:keyId/revokeintegration_client_keysCreate/update/rotate/test a webhook Admin Office POST/PATCH .../webhooks/**integration_webhook_endpointsRetry a delivery Admin Office POST .../webhook-deliveries/:id/retryintegration_webhook_deliveriesRead the catalogue/structures/modifiers Accounting app System GET /api/integration/fees/{categories,components,structures,scholarships,charges}— (read-only) Read the school's own identity and contact facts Accounting app System GET /api/integration/fees/school— (read-only; bankName/bankAccountNumber need payroll:bank:read in addition to fees:read) Read students/obligations Accounting app System GET /api/integration/fees/{students,obligations}— Report a payment Accounting app System POST /api/integration/fees/paymentsfee_payment_sync_events (see Fees module)Manage mappings Accounting app System PUT /api/integration/fees/{student,invoice}-mappings/:idfee_external_student_mappings/fee_external_invoice_mappingsPoll the change feed Accounting app System GET /api/integration/fees/changesRedis poll marker Receive a webhook Accounting app's receiver System Signed POST — Deliver/retry a webhook Worker System IntegrationWebhookJob.DELIVERintegration_webhook_deliveriesReconcile after a restore System Backup restore IntegrationRestoreReconciler.reconcileNew feed epoch, keys revoked, webhooks paused
The office creates one client per external accounting system, scopes it, then issues an API key the accounting vendor configures into their application.
ACCOUNTING_INTEGRATION_ENABLED is true on the server (otherwise creating a client or issuing a key answers 404 ACCOUNTING_INTEGRATION_DISABLED).
INTEGRATION_SECRET_KEYS is set on the server (otherwise every write here answers 409 INTEGRATION_NOT_CONFIGURED).
The caller holds FeeIntegrations_CREATE.
Step Actor/System Action Result Source 1 Office POST /api/integration-clients with name, scopes, optional rate limitA client row, status: active IntegrationClientsService.create2 Office POST /api/integration-clients/:id/keysA key issued; the raw token returned ONCE in the response body, Cache-Control: no-store IntegrationKeysService.issue3 Office Copies the token into the accounting vendor's configuration — — 4 Accounting app GET /api/integration/fees/ping with the tokenConfirms the key, scopes, server time, and feed epoch integration-fees-meta.controller.ts
Branch Condition Behavior Error/Result Integration not configured INTEGRATION_SECRET_KEYS unsetEvery credential-issuing write refused 409 INTEGRATION_NOT_CONFIGURED; reads that need no new credential still workDuplicate name Another client already has this name (case-insensitive) Refused before any write 409 INTEGRATION_CLIENT_NAME_TAKENUnknown scope A scope not in INTEGRATION_SCOPES Refused 400 VALIDATION_FAILEDThird active key A client already holds 2 active, unexpired keys Refused — rotate or revoke one first 409 INTEGRATION_KEY_LIMITKey rotation replacesKeyId passed on issueThe old key's life is shortened to a 7-day grace window rather than killed immediately Both keys authenticate during the grace period
The vendor's system posts a payment event whenever money moves on their side; the API acknowledges receipt immediately and processes it asynchronously.
Step Actor/System Action Result Source 1 Accounting app POST /api/integration/fees/payments with eventId, transactionRef, status, amountPaisa, externalUpdatedAt, and an identity hint202 with {syncEventId, replayed}IntegrationFeesPaymentsController2 Backend Matches identity (obligation/student/invoice ref) against mappings; writes/updates fee_external_transactions Applied, held, or rejected — see the Fees module's payment sync docs FeeSyncProcessorService (owned by fees, invoked here)3 Accounting app GET /api/integration/fees/payments/events/:eventIdThe event's current processing state IntegrationFeesPaymentsController
Branch Condition Behavior Error/Result Replay The same eventId posted again No second write; replayed: true 202, same syncEventIdReused eventId, different body Same eventId, different payload Refused — not a true replay FEE_SYNC_EVENT_ID_REUSED (see Fees module)Body too large Content-Length over 32 KBRefused before the body is read 413 INTEGRATION_PAYLOAD_TOO_LARGEWrong scope The key lacks payments:write Refused 403 INTEGRATION_SCOPE_MISSINGRate limited Over the client's per-minute budget Refused with Retry-After 429, header set
Whenever a subscribed change happens, the school's server signs and POSTs a thin event to every live endpoint the client has registered for it.
Step Actor/System Action Result Source 1 System A fee/structure/modifier/payment/mapping change writes an integration_change_events row in the SAME transaction as the business change The change is durable before anything is sent FeeChangeFeedService.record (Fees module)2 Worker FeeJob.CHANGE_FANOUT reads subscribed, live endpoints for the event's scopeOne integration_webhook_deliveries row per matching endpoint IntegrationWebhookFanoutService3 Worker IntegrationWebhookJob.DELIVER POSTs {id, type, occurredAt, resource, academicSessionId}, signed2xx → succeeded; else scheduled for retry per the backoff table IntegrationWebhookDeliveryService4 Accounting app receiver Verifies X-Skoolsewa-Signature, processes, fetches the full resource by its public id if needed The event never carries the resource itself —
Branch Condition Behavior Error/Result Endpoint paused The endpoint's status is paused The delivery is HELD an hour, no attempt spent — Client suspended The owning client is not active Same — held, not killed — Endpoint disabled status = disabledThe delivery dies immediately lastError: "endpoint_disabled"Nine failed attempts Every backoff step exhausted dead, a job_failures row, office notifiedintegration.webhook_dead.admin50 dead deliveries in a row consecutiveFailures reaches the thresholdThe endpoint auto-disables itself integration.webhook_disabled.adminSecret rotation A rotation is in its 24-hour grace window Deliveries are signed with BOTH secrets The header carries two v1= values Client-private event e.g. student_mapping.created Delivered only to the client the mapping belongs to Never fanned to every subscriber
Route: POST /api/integration-clients/:id/webhooks/:webhookId/rotate-secret. The previous secret keeps signing for 24 hours (previous_secret_ciphertext/previous_valid_until), so a receiver switching secrets never drops an event mid-rotation. The new secret is shown once, Cache-Control: no-store.
From Event To Guard Side Effects activeSuspend suspended— Keys stop authenticating; webhooks hold their deliveries suspendedActivate active— — active/suspendedRevoke revokedReason required Terminal; cascades key revocation and webhook disabling
active (issued) → revoked (manual revoke, client revocation, or a restore). A key's expires_at (365 days) is a second, independent path to unusability that never changes status — usable() checks both.
From Event To Side Effects activePause pausedDeliveries hold, no attempt spent pausedRe-enable activedispatch_generation bumped, consecutive_failures resetactive/pausedDisable (manual or auto at 50 dead) disabledDeliveries die on next attempt disabledRe-enable activeSame as above
Flow DB Writes Jobs Notifications Issue a key integration_client_keys— — Create a webhook integration_webhook_endpoints— — A fee change integration_change_events (Fees module)CHANGE_FANOUT— Fan-out integration_webhook_deliveriesIntegrationWebhookJob.DELIVER per delivery— Delivery dead integration_webhook_deliveries, job_failures— integration.webhook_dead.adminEndpoint auto-disabled integration_webhook_endpoints— integration.webhook_disabled.adminRestore New feed epoch, keys revoked, webhooks paused — integration.restored.admin
Scenario Trigger Recovery Integration not configured INTEGRATION_SECRET_KEYS unsetOperator sets the env var, restarts; existing (already-issued) credentials remain unusable until then Key limit reached 2 active keys already Office revokes or rotates one before issuing a third Webhook delivery exhausted 9 attempts, all failed Office fixes the receiver, then POST .../retry, or waits for the daily recovery pass Restore A backup is restored Every credential invalidated by construction; office re-issues what the accounting app needs. If reconciling the integration fails, the site stays in maintenance rather than lifting it over stale credentials — restarting the API retries reconciliation at boot Restore outside the normal flow Someone restores the database by hand (pg_restore onto the same cluster), bypassing the backup-restore workflow Detected at the next boot from either an active key/client the system already recorded revoking, or the change feed having served further than the restored data now goes — reconciled the same way as a workflow-driven restore Accounting integration switched off An operator sets ACCOUNTING_INTEGRATION_ENABLED=false and restarts The integration API, the fees/salary/asset accounting-facing admin routes, and every client/key/webhook-creating admin action answer 404 ACCOUNTING_INTEGRATION_DISABLED; listing, suspending, revoking a client, and revoking a key stay reachable. Nothing new is written to the change feed or delivered as a webhook, and every outstanding delivery is killed rather than retried. Turning the switch back on starts a fresh feed epoch, so the accounting app resyncs from the current state instead of trying to resume a stale cursor
Feature Minor Behavior Trigger Result Source Key authentication Constant-time comparison regardless of whether the prefix exists Every request An unknown prefix and a wrong secret answer identically (401) IntegrationKeysService.authenticateKey last_used_at Written at most once a minute Every authenticated request Avoids a write storm on a busy integration IntegrationKeysService.touchRate limit Per-client, per-minute, Retry-After header Every request over budget 429IntegrationRateLimiterServiceIP throttle 600/min per IP, AHEAD of authentication Every request Bounds unauthenticated noise before a key is even checked IntegrationEndpoint decoratorScope-less route A route with no declared scope Any such route (should not exist) Refused outright, logged as an error IntegrationApiKeyGuardBody size cap >32 KB declared Content-Length Any POST/PUT 413, unreadIntegrationApiKeyGuardCursor canonicalization A non-canonical base64url cursor Any keyset-paged GET 400 INTEGRATION_CURSOR_INVALID — never a best-effort guessintegration-fees-keyset.util.tsChange feed epoch A cursor from before a restore GET .../changes410 INTEGRATION_CURSOR_EXPIRED — resync from scratchIntegrationFeedServicePrivate event visibility student_mapping.* eventsThe feed and fan-out Visible/delivered only to the owning client INTEGRATION_CLIENT_PRIVATE_EVENT_TYPESTwo-secret signing A rotation's 24-hour grace window Every delivery in that window The header carries two v1= signatures webhookSignatureHeader
Rule Business Reason Enforced In Tests A client is never re-created for the same accounting system Payment sync idempotency (eventId) is per client integration_clients is keyed by name uniqueness, keys/webhooks rotate INSIDE it— A key's secret is never recoverable Only an HMAC under a server pepper is stored integration_client_keys.secret_hashapi-key.util.spec.tsA webhook secret must be decryptable (not just verifiable) The server signs outbound requests with it AES-256-GCM under the keyring, not a one-way hash secret-box.util.spec.tsA restore invalidates every credential Data issued before a restore predates the restored state by construction IntegrationRestoreReconciler— Scholarship/charge lines never leak an internal id Public surfaces reference public_id only Every fees-api mapper —
Flow Edge Case Expected Behavior Source Authenticate Malformed Authorization header 401 INTEGRATION_AUTH_INVALID, same as a wrong secretIntegrationApiKeyGuardWebhook create 6th endpoint for one client 409 INTEGRATION_WEBHOOK_LIMITIntegrationWebhookEndpointsServiceWebhook create URL with credentials or a fragment 400 INTEGRATION_WEBHOOK_URL_INVALIDwebhook-target.util.tsWebhook create http:// in productionRefused unless INTEGRATION_WEBHOOK_ALLOW_PRIVATE_TARGETS=true Same Mapping upsert A student already mapped Old mapping retired, new one created — never an in-place overwrite fee_external_student_mappings (Fees module)Feed read No cursor supplied Starts from the beginning of what is visible below the snapshot's xmin IntegrationFeedService.read
Backend doc: /docs/developer/integration/backend
API doc: /docs/developer/integration/api
Fee data itself: /docs/developer/fees/feature
Complete API contracts for the asset management module, including routes, auth, DTOs, responses, errors, and integration notes.
Accounting Integration Backend Documentation
Backend architecture, data model, credentials, webhooks, the change feed, and operational behavior for the accounting integration module.