Skip to main content

DemozPay — HTTP API Inventory

Purpose. A single audit-friendly catalogue of every HTTP endpoint the API exposes today, grouped by module, with auth requirements, source file, and Live / Partial / Stub / Planned status per the STATUS_LEGEND.

Scope. Backend HTTP only (the NestJS modular monolith at apps/api/). Frontends, gRPC, Kafka events, and CLI tools are out of scope.

How to use. Use this to spot gaps before sprint-planning. Each section ends with a Gaps block. When you ship a new endpoint, add a row here in the same commit.


What changed in this audit pass (2026-05-29)

The six findings called out in the first inventory have all been closed in code:

#FindingStatusNotes
1Double-prefix bug (@Controller('api/...') + globalPrefix='api'/api/api/...)Fixed12 controllers renamed; supertest spec now calls app.setGlobalPrefix('api') so URLs match production.
2Lending had no read endpointsFixedGET /api/loans + GET /api/loans/:id shipped via ListLoansQuery + GetLoanQuery.
3No reversal for one-time earnings post-approvalFixed (alias)POST :id/reverse added as an operator-vocabulary alias for /void. The state machine already permitted POSTED → VOIDED.
4No employee self-service surfacePartially fixed/api/me/profile, /api/me/loans, /api/me/loans/:id shipped. /me/ewa/requests + /me/payslips follow when the EWA + payroll repos gain findByEmployee.
52FA + magic-link plugins not wiredFixedtwoFactor() + magicLink() registered in better-auth.factory.ts. TwoFactor table already existed.
6No bank-webhook replay surfaceFixedNew bank_webhook_event table; receive-side audit logging; GET/POST /api/integration/webhooks(/...) platform-admin surface.

Plus a Tier-2 gap closed inline: KYC POST :id/request-more-info.


Conventions

SymbolMeaning
🔓 Public@Public() — no session required (webhooks, health, metrics).
🔐 SessionDefault — any signed-in user.
👤 Org:X@RequireOrgRole(...) — caller must hold role X in the active organization (member / admin / owner).
🛡 Platform@RequirePlatformAdmin() — cross-tenant admin (DemozPay staff).
🪙 IdemMoney-moving — Idempotency-Key header required (ADR-007).

Global URL conventions

  • Global prefix: apps/api/src/main.ts:46 sets app.setGlobalPrefix('api'). Every Nest controller mounts under /api/. Controllers MUST be declared without a leading api/ in @Controller(...) — otherwise the route doubles up (Finding #1).
  • Auth subtree: better-auth is mounted on Express before Nest's router at /api/auth/* (apps/api/src/main.ts:41-42). It owns its full subtree.

Module index

ModuleStatusControllersSee
Auth (better-auth)Live(Express handler)§1
Platform / Health / MetricsLive3§2
Business (organizations)Live1§3
EmployeesLive1§4
Me / self-serviceLive1§5
Integration (bank webhooks)Live2§6
KYCLive1§7
SanctionsLive1§8
EWA (Earned-Wage Access)Live1§9
Lending (salary-backed loans)Live1§10
Payroll (core lifecycle)Live6§11
Payroll (P2 — earnings / overtime / protection)Live1§12
Payroll (adjustments)Live1§13
Payroll (deduction mandates)Live1§14
Payroll (tenant settings + auto-lock policy)Live2§15
Payroll (PDF payslips)Live1§16
Payroll (court-order remit)Live1§17
Payroll (platform-admin overrides)Live1§18
Payroll (health)Live1§19
BNPLPlanned§20
Wallet (read-only over ledger)Planned§21
Equb / SavingsPlanned§22
NotificationsStub§23

1. Auth (better-auth)

Mount. Express owns /api/auth/* before Nest takes effect — apps/api/src/main.ts:41-42. Routes are produced by better-auth itself (apps/api/src/identity/auth/better-auth.factory.ts:33-150).

Plugins enabled (post-Finding-#5): emailAndPassword, emailVerification, organization, phoneNumber (OTP), twoFactor, magicLink.

MethodPathPluginStatusNotes
POST/api/auth/sign-up/emailemail+pwLiveOrg plugin auto-creates first org.
POST/api/auth/sign-in/emailemail+pwLiveSets session cookie.
POST/api/auth/sign-outcoreLive
GET/api/auth/get-sessioncoreLiveReturns current session + user.
POST/api/auth/forget-passwordemail+pwLiveFires sendResetPassword.
POST/api/auth/reset-passwordemail+pwLive
POST/api/auth/verify-emailemailVerificationLiveToken from sendVerificationEmail.
POST/api/auth/phone-number/send-otpphoneNumberLiveSMS via pluggable sender.
POST/api/auth/phone-number/verifyphoneNumberLive
POST/api/auth/two-factor/enabletwoFactorLivePer-user TOTP.
POST/api/auth/two-factor/disabletwoFactorLive
POST/api/auth/two-factor/verifytwoFactorLive
POST/api/auth/two-factor/generate-backup-codestwoFactorLive
POST/api/auth/sign-in/magic-linkmagicLinkLiveEmail link, 15-minute expiry.
POST/api/auth/magic-link/verifymagicLinkLive
POST/api/auth/organization/createorganizationLiveNew tenant.
GET/api/auth/organization/listorganizationLiveCaller's orgs.
POST/api/auth/organization/set-activeorganizationLiveSets active tenant.
GET/api/auth/organization/get-full-organizationorganizationLiveMembers + invites.
POST/api/auth/organization/invite-memberorganizationLive
POST/api/auth/organization/accept-invitationorganizationLive
POST/api/auth/organization/leaveorganizationLive
POST/api/auth/organization/update-member-roleorganizationLive
POST/api/auth/organization/remove-memberorganizationLive

Gaps remaining.

  • Tenant-level 2FA enforcement policy (force-enable for all admins of a tenant) — Planned. The plugin is wired per-user only.
  • Passkey / WebAuthn — Planned. Not required by current threat model.

2. Platform / Health / Metrics

MethodPathAuthFile:lineNotes
GET/api/🔓 Publicapps/api/src/app/app.controller.ts:8-12Hello-world; banner only.
GET/api/healthz🔓 Publicapps/api/src/_infra/health/health.controller.ts:25Liveness — 200 if event loop runs.
GET/api/readyz🔓 Publicapps/api/src/_infra/health/health.controller.ts:35Probes PG + Redis + Kafka + ledger gRPC.
GET/api/metrics🔓 Publicapps/api/src/_infra/observability/metrics/metrics.controller.ts:13-19Prometheus scrape.

3. Business

@Controller('business')/api/business/*. Source: apps/api/src/business/business.controller.ts.

MethodPathAuthLineNotes
POST/api/business🛡 Platform38Create business.
GET/api/business🔐 Session61List businesses caller belongs to.
GET/api/business/:id👤 Org:member+74
GET/api/business/:id/employee-count👤 Org:member+84
PUT/api/business/:id👤 Org:admin+91
PUT/api/business/:id/verify-kyc🛡 Platform101
PUT/api/business/:id/status🛡 Platform111
DELETE/api/business/:id👤 Org:owner124

Gaps.

  • GET /api/business/:id/branding — multi-tenant branding (logo, primary colour). Planned.
  • Soft-delete + restore — current DELETE is destructive. Planned.

4. Employees

@Controller('employees')/api/employees/*. Source: apps/api/src/workforce/employee/employee.controller.ts.

MethodPathAuthLineNotes
POST/api/employees👤 Org:admin+42
POST/api/employees/bulk-import👤 Org:admin+89CSV upload.
GET/api/employees👤 Org:member+135
GET/api/employees/active/:businessId👤 Org:member+141
GET/api/employees/department/:businessId/:department👤 Org:member+147
GET/api/employees/:id👤 Org:member+156
PUT/api/employees/:id👤 Org:admin+168
PUT/api/employees/:id/status👤 Org:admin+188
DELETE/api/employees/:id👤 Org:admin+208

Gaps (Tier-2 follow-up — not blocking).

  • POST /api/employees/:id/transfer — change businessId / department within the same tenant. Needs an audit row + outbox event.
  • GET /api/employees/:id/employment-history — paginated audit timeline. Backing table doesn't exist yet.
  • GET /api/employees/export — paginated CSV for payroll reconciliation. Trivial; deferred.

5. Me / employee self-service

NEW in this pass (Finding #4). @Controller('me')/api/me/*. Source: apps/api/src/identity/me/me.controller.ts.

Read-only. Resolves session.user.id → Employee via Employee.userId, then pins downstream queries to that employee id. Returns 404 EMPLOYEE_NOT_LINKED when the caller has no employee row (org-admin / platform-admin staff).

MethodPathAuthLineNotes
GET/api/me/profile🔐 Session51Caller's employee row.
GET/api/me/loans🔐 Session66Caller's loans (via ListLoansQuery).
GET/api/me/loans/:id🔐 Session87Same 404 on cross-employee leak.

Gaps (next commits).

  • GET /api/me/ewa/requests — blocked on EwaRequestRepository.findByEmployee. Mechanical port addition.
  • GET /api/me/payslips — blocked on PayrollRunEntryRepository.findByEmployee. Mechanical.
  • GET /api/me/sanctions — should always 200 with empty (employees should not see their own screen results except via KYC verdict).
  • GET /api/me/notifications — depends on Notifications module (§23).

6. Integration (bank webhooks)

Source: apps/api/src/money/integration/.

MethodPathAuthFile:lineNotes
POST/api/integration/bank-callback/:partner🔓 Public + HMACbank-webhook.controller.ts:60Inbound partner webhook. Now persists every accepted payload to bank_webhook_event for replay (Finding #6).
GET/api/integration/webhooks🛡 Platformbank-webhook-replay.controller.ts:40NEW — list recent audit rows.
GET/api/integration/webhooks/:id🛡 Platformbank-webhook-replay.controller.ts:48NEW — single row with rawBody.
POST/api/integration/webhooks/:id/replay🛡 Platformbank-webhook-replay.controller.ts:60NEW — re-applies the stored outcome via BankSettlementApplier. Idempotent at the applier layer.

Gaps.

  • POST /api/integration/bank-callback/:partner/test — partner-driven smoke ping. Planned.

7. KYC

@Controller('kyc')/api/kyc/*. Source: packages/kyc/backend/presentation/kyc.controller.ts.

MethodPathAuthLineNotes
POST/api/kyc🔐 Session73Submit.
POST/api/kyc/:id/claim🛡 Platform99
POST/api/kyc/:id/approve🛡 Platform107Sanctions screen at approve (E2).
POST/api/kyc/:id/reject🛡 Platform128Terminal.
POST/api/kyc/:id/request-more-info🛡 Platform148NEW — returns to PENDING with an applicant-facing note. Emits kyc.more_info_requested.v1.
GET/api/kyc/queue🛡 Platform162Reviewer queue.
GET/api/kyc/status🔐 Session173Caller's own KYC state.

Gaps.

  • GET /api/kyc/:id/documents — signed-URL fetch for uploaded docs. Planned.
  • Webhook to notify partner FIs of KYC verdict. Planned.
  • POST /api/kyc/:id/escalate — second-line reviewer override.

8. Sanctions

@Controller('sanctions'). Source: packages/sanctions/backend/presentation/sanctions.controller.ts.

MethodPathAuthLineNotes
POST/api/sanctions/screen🛡 Platform42Ad-hoc; also runs at KYC approve.
GET/api/sanctions/checks🛡 Platform67Recent screens.

Gaps.

  • GET /api/sanctions/checks/:id — single-check detail with match evidence.
  • POST /api/sanctions/lists/refresh — HTTP trigger for OFAC/UN ingest (CLI exists today).

9. EWA (Earned-Wage Access)

@Controller('ewa')/api/ewa/*. Source: packages/ewa/backend/presentation/ewa.controller.ts.

MethodPathAuthLineNotes
GET/api/ewa/eligibility👤 Org:admin+61
POST/api/ewa/requests🪙 Idem · 👤 Org:admin+79
POST/api/ewa/requests/:id/disburse🪙 Idem · 👤 Org:admin+109
POST/api/ewa/requests/:id/record-repayment🪙 Idem · 👤 Org:admin+135

Gaps (Tier-2 follow-up).

  • GET /api/ewa/requests — list past requests per tenant / employee. Needs EwaRequestRepository.findByTenant (same shape as the new LoanRepository.findByTenant).
  • GET /api/ewa/requests/:id — single read.
  • POST /api/ewa/requests/:id/cancel — pre-disburse cancellation.
  • Employee-facing self-service draw — today the admin draws on the employee's behalf.

10. Lending

@Controller('loans')/api/loans/*. Source: packages/lending/backend/presentation/lending.controller.ts.

MethodPathAuthLineNotes
GET/api/loans👤 Org:member+64NEW — list, supports ?limit=, ?status= (repeatable), ?borrowerId=.
GET/api/loans/:id👤 Org:member+83NEW — single read; 404 with stable code.
POST/api/loans/quote👤 Org:admin+95Affordability quote.
POST/api/loans🪙 Idem · 👤 Org:admin+112Originate.
POST/api/loans/:id/disburse🪙 Idem · 👤 Org:admin+145
POST/api/loans/:id/installments/:idx/remit-to-fi🪙 Idem · 👤 Org:admin+175
POST/api/loans/:id/installments/:idx/record-repayment🪙 Idem · 👤 Org:admin+209

Gaps.

  • POST /api/loans/:id/restructure — change tenor / installment plan. Planned.
  • POST /api/loans/:id/write-off — non-recoverable. Planned.
  • Hardship / pause endpoint. Planned.

11. Payroll — core lifecycle

@Controller('payroll')/api/payroll/*. Source: packages/payroll/backend/presentation/payroll.controller.ts. Auth: 👤 Org:admin+.

MethodPathLine
POST/api/payroll102
POST/api/payroll/:id/calculate129
POST/api/payroll/:id/approve143
POST/api/payroll/:id/disburse (🪙 Idem)160
POST/api/payroll/:id/cancel177
POST/api/payroll/:id/lock292
GET/api/payroll/:id/report187
GET/api/payroll/:id/report.csv193
GET/api/payroll/:id/report.mor.xml215
GET/api/payroll/:id/report.pension.xml234
GET/api/payroll/:id/payslips/:employeeId258
GET/api/payroll/:id/payslips/:employeeId.html268
GET/api/payroll304
GET/api/payroll/:id312

Gaps (Tier-2 follow-up).

  • GET /api/payroll/:id/audit — full audit trail (actor, timestamps, state transitions). Backing data is in the outbox + audit log; needs a read view.
  • Differential / incremental run (re-calc only affected employees). Planned.

12. Payroll — P2 (earnings, overtime, protection)

@Controller('payroll') + p2 path prefixes. Source: packages/payroll/backend/presentation/payroll-p2.controller.ts. Auth: 👤 Org:admin+.

MethodPathLine
POST/api/payroll/protection-policies84
POST/api/payroll/protection-policies/:id/activate106
GET/api/payroll/protection-policies118
POST/api/payroll/one-time-earnings144
POST/api/payroll/one-time-earnings/:id/approve173
POST/api/payroll/one-time-earnings/:id/void185
POST/api/payroll/one-time-earnings/:id/reverse202
GET/api/payroll/one-time-earnings218
POST/api/payroll/overtime-policies251
POST/api/payroll/overtime-policies/:id/activate278
GET/api/payroll/overtime-policies290
POST/api/payroll/overtime-entries317
POST/api/payroll/overtime-entries/:id/approve351
GET/api/payroll/overtime-entries363

13. Payroll — adjustments

@Controller('payroll/adjustments'). Auth: 👤 Org:admin+.

MethodPathLine
POST/api/payroll/adjustments59
POST/api/payroll/adjustments/:id/approve89
POST/api/payroll/adjustments/:id/post101
POST/api/payroll/adjustments/:id/reverse113
POST/api/payroll/adjustments/:id/void132
GET/api/payroll/adjustments148
GET/api/payroll/adjustments/:id168

14. Payroll — deduction mandates

@Controller('payroll/deduction-mandates'). Auth: 👤 Org:admin+.

MethodPathLine
POST/api/payroll/deduction-mandates57
POST/api/payroll/deduction-mandates/:id/activate91
POST/api/payroll/deduction-mandates/:id/suspend103
POST/api/payroll/deduction-mandates/:id/resume119
POST/api/payroll/deduction-mandates/:id/cancel131
GET/api/payroll/deduction-mandates147
GET/api/payroll/deduction-mandates/:id165

15. Payroll — tenant settings + auto-lock policy

MethodPathAuthFile:line
POST/api/payroll/tenant-settings👤 Org:admin+tenant-settings.controller.ts:80
GET/api/payroll/tenant-settings👤 Org:admin+:102
GET/api/payroll/tenant-settings/effective👤 Org:admin+:116
DELETE/api/payroll/tenant-settings👤 Org:admin+:128
POST/api/payroll/auto-lock-policy👤 Org:admin+auto-lock-policy.controller.ts:52
GET/api/payroll/auto-lock-policy👤 Org:admin+:83
GET/api/payroll/auto-lock-policy/effective👤 Org:admin+:97
DELETE/api/payroll/auto-lock-policy👤 Org:admin+:119

16. Payroll — PDF payslips

@Controller('payroll'). Source: apps/api/src/payroll/payroll-pdf.controller.ts. Auth: 👤 Org:admin+.

MethodPathLine
GET/api/payroll/:id/payslips/:employeeId.pdf51
GET/api/payroll/:id/payslips.zip100

17. Payroll — court-order remit

@Controller('payroll'). Source: apps/api/src/payroll/consumers/court-order-remit.controller.ts. Auth: 👤 Org:admin+ (POST also 🪙 Idem).

MethodPathLine
GET/api/payroll/:id/report.court-orders.json44
GET/api/payroll/:id/report.court-orders.xml58
POST/api/payroll/:id/report.court-orders/submit86

Background poller: CourtOrderAutoSubmitService, state at /api/health/payroll.


18. Payroll — platform-admin overrides

@Controller('platform/payroll'). Auth: 🛡 Platform.

MethodPathLine
POST/api/platform/payroll/auto-lock-policy/:tenantId48
GET/api/platform/payroll/auto-lock-policy/:tenantId75
DELETE/api/platform/payroll/auto-lock-policy/:tenantId91
GET/api/platform/payroll/auto-lock-policies103
GET/api/platform/payroll/tenant-settings118
POST/api/platform/payroll/tenant-settings/:tenantId137
GET/api/platform/payroll/tenant-settings/:tenantId160
DELETE/api/platform/payroll/tenant-settings/:tenantId174

19. Payroll — health

@Controller('health/payroll'), @Public(). Source: apps/api/src/payroll/payroll-health.controller.ts.

MethodPathLine
GET/api/health/payroll17 — {ready, healthy, crons[], generatedAt}. Crons: payroll-auto-lock, court-order-auto-submit, payroll-deductions-poller.

20. BNPL

Status: Planned. packages/bnpl/ not yet scaffolded. Domain sketch:

Aggregates. Agreement (DRAFT → ACCEPTED → ACTIVE → SETTLED / DEFAULTED). Instalment (one per scheduled date). ADR fit. Same idempotency + outbox model as lending; ledger postings via the same LedgerPort shape. No derived outstanding column — derived from the ledger (ADR-006). Storage. bnpl_agreement, bnpl_instalment, bnpl_merchant_partner. Money columns: NUMERIC(20,0) in santim.

Routes (planned).

MethodPathNotes
POST/api/bnpl/quotesAffordability + plan offer.
POST/api/bnpl/agreements🪙 Idem — accept + sign.
GET/api/bnpl/agreementsList per tenant.
GET/api/bnpl/agreements/:id
POST/api/bnpl/agreements/:id/installments/:idx/collect🪙 Idem — payroll-pull or wallet-debit.
POST/api/bnpl/agreements/:id/cancelPre-activation only.
POST/api/bnpl/agreements/:id/restructureOperator-only.
GET/api/me/bnpl/agreementsSelf-service.
POST/api/integration/merchant-callback/:partner🔓 Public + HMAC; merchant settlement.

21. Wallet

Status: Planned read-only over the ledger. Per ADR-006 there is no Wallet aggregate — the ledger is source of truth. The wallet surface is a thin read view (SELECT … FROM ledger_entry WHERE account_id = …).

Routes (planned).

MethodPathNotes
GET/api/wallet/balanceDerives from the ledger for the caller's wallet account.
GET/api/wallet/transactionsPaginated; ?from=&to=&direction=.
GET/api/me/wallet/balanceSelf-service alias.
GET/api/me/wallet/transactions
POST/api/wallet/transfer🪙 Idem — wallet→wallet (KYC + sanctions gated). Cross-domain choreography across ledger + integration-gateway.

No new schema — re-uses ledger_account + ledger_entry.


22. Equb / Savings

Status: Planned. No code today.

Aggregates. EqubGroup, EqubMembership, EqubCycle. SavingsGoal, SavingsContribution. ADR fit. Group payouts are choreographed wallet→wallet transfers; the ledger does the money work. Storage. equb_group, equb_member, equb_cycle, savings_goal, savings_contribution. All NUMERIC(20,0) santim.

Routes (planned).

MethodPathNotes
POST/api/equb/groupsCreate.
GET/api/equb/groupsList.
POST/api/equb/groups/:id/join
POST/api/equb/groups/:id/leave
POST/api/equb/groups/:id/contribute🪙 Idem
POST/api/equb/groups/:id/payout/:cycle🪙 Idem — selected member receives the pot.
GET/api/equb/groups/:id/cycles
POST/api/savings/goalsPersonal savings goal.
POST/api/savings/goals/:id/deposit🪙 Idem
POST/api/savings/goals/:id/withdraw🪙 Idem
GET/api/me/savings/goalsSelf-service.

23. Notifications

Status: Stub. The Go services/notifications only exposes /health. No HTTP surface in apps/api/ yet.

Planned consumer behaviour. Subscribes to payroll.deductions_taken.v1, kyc.approved.v1, kyc.rejected.v1, kyc.more_info_requested.v1 (NEW), loan.disbursed.v1, ewa.disbursed.v1. Dispatches SMS / email / push per per-user preferences.

Routes (planned).

MethodPathNotes
GET/api/notifications/preferencesCaller's channel prefs.
PUT/api/notifications/preferencesToggle SMS / email / push per category.
POST/api/notifications/test🛡 Platform — send a test message.
GET/api/me/notificationsCaller's inbox (read view over Kafka topic).

Audit findings (this pass)

All six findings from the prior pass are closed. New follow-ups surfaced during fixes:

  1. /me/ewa/requests + /me/payslips are blocked on repo extensions. Each is one findByEmployee method on the relevant repository — same shape as the new LoanRepository.findByTenant({borrowerId}). Mechanical.
  2. No DB migration verification for bank_webhook_event. The migration is shipped (20260601000000_bank_webhook_event) but the CI smoke (payroll-poller-integration workflow) doesn't include a non-payroll table sanity check. Add a verify-bank-webhook-migration.sh and call it from CI.
  3. request-more-info re-uses rejectionReason slot for the applicant-facing note. Acceptable for the pilot but a dedicated reviewerNote column will be cleaner — tracked alongside the reviewer-notes refactor.
  4. Tenant-level 2FA enforcement — the plugin is wired per-user only. A tenant policy ("every admin must have 2FA") needs a new table and a guard.
  5. EWA / payroll list endpoints still missing (see Tier-2 follow-up in §9, §11).
  6. No supertest coverage for the new replay + me routes. Each is small; add specs alongside the existing platform-admin.supertest.spec.ts pattern.

Maintenance

  • Every new controller / new route MUST add a row here in the same PR.
  • When a Planned row in a Gaps block ships, move it into the table above and update the count in the Module index.
  • Re-run the audit script periodically: grep -rn '@Controller\|@Get\|@Post\|@Put\|@Patch\|@Delete' apps/api/src packages/*/backend/presentation should match this file row-for-row.