API Layer & Hooks
The portal uses a thin HTTP client for API communication and React Query hooks for server state management.
HTTP Client
apiFetch(path, options?)
Generic async HTTP wrapper in api/client.ts:
- Prepends
/api/v1 to the path
- Automatically injects CSRF token from cookies as
X-CSRFToken header
- Sets
Content-Type: application/json for request bodies
- Includes credentials (
credentials: "include") for session cookies
- Returns parsed JSON response
- Returns
undefined for 204 No Content
- Throws
ApiHttpError on non-2xx responses
ApiHttpError
Custom error class with structured error data:
class ApiHttpError extends Error {
status: number;
body: ApiError;
}
interface ApiError {
message?: string;
detail?: string;
code?: string;
errors?: Record<string, string>; // field-level errors
}
ensureCsrf()
Hits GET /api/v1/auth/session to establish the CSRF cookie before login.
Auth API (api/auth.ts)
| Function |
Method |
Path |
Description |
getSession() |
GET |
/auth/session |
Establish CSRF cookie |
login(email, password) |
POST |
/auth/login |
Authenticate |
logout() |
DELETE |
/auth/session |
End session |
getMe() |
GET |
/portal/me |
Get borrower profile |
Portal API (api/portal.ts)
41 functions organized by domain:
Configuration & Dashboard
| Function |
Method |
Path |
Returns |
getBranding() |
GET |
/portal/config |
TenantBranding |
getDashboard() |
GET |
/portal/dashboard |
DashboardData |
Loans
| Function |
Method |
Path |
Returns |
getLoans() |
GET |
/portal/loans |
LoanSummary[] |
getLoan(loanId) |
GET |
/portal/loans/{id} |
LoanDetail |
getAmortization(loanId) |
GET |
/portal/loans/{id}/amortization |
AmortizationEntry[] |
Payments
| Function |
Method |
Path |
Returns |
getPayments(loanId) |
GET |
/portal/loans/{id}/payments |
Payment[] |
previewPayment(loanId, input) |
POST |
/portal/loans/{id}/payments/preview |
PaymentPreview |
submitPayment(loanId, input) |
POST |
/portal/loans/{id}/payments |
Payment |
Payment Instruments
| Function |
Method |
Path |
Returns |
getInstruments() |
GET |
/portal/payment-instruments |
PaymentInstrument[] |
addInstrument(input) |
POST |
/portal/payment-instruments |
PaymentInstrument |
deleteInstrument(id) |
DELETE |
/portal/payment-instruments/{id} |
void |
Autopay
| Function |
Method |
Path |
Returns |
getAutopay(loanId) |
GET |
/portal/loans/{id}/autopay |
AutopayPlan \| null |
enrollAutopay(loanId, input) |
POST |
/portal/loans/{id}/autopay |
AutopayPlan |
cancelAutopay(loanId, autopayId) |
POST |
.../autopay/{id}/cancel |
AutopayPlan |
enableAutopay(loanId, autopayId) |
POST |
.../autopay/{id}/enable |
AutopayPlan |
Documents
| Function |
Method |
Path |
Returns |
getDocuments(type?) |
GET |
/portal/documents |
PortalDocument[] |
getDownloadUrl(id) |
GET |
/portal/documents/{id}/download |
{ download_url } |
requestUploadLink(input) |
POST |
/portal/documents/upload |
UploadLinkResponse |
confirmUpload(id) |
POST |
/portal/documents/{id}/confirm |
void |
Profile
| Function |
Method |
Path |
Returns |
getProfileDetail() |
GET |
/portal/profile |
ProfileDetail |
updateProfile(input) |
POST |
/portal/profile |
ProfileDetail |
updateCommPreferences(input) |
POST |
/portal/profile/communication-preferences |
ProfileDetail |
Support Cases
| Function |
Method |
Path |
Returns |
getCases() |
GET |
/portal/support/cases |
SupportCase[] |
createCase(input) |
POST |
/portal/support/cases |
SupportCase |
getCase(caseId) |
GET |
/portal/support/cases/{id} |
SupportCaseDetail |
addCaseMessage(caseId, input) |
POST |
/portal/support/cases/{id}/message |
CaseInteraction |
Hardship
| Function |
Method |
Path |
Returns |
getHardshipApplications() |
GET |
/portal/hardship/applications |
HardshipApplication[] |
getHardshipApplication(id) |
GET |
/portal/hardship/applications/{id} |
HardshipApplication |
submitHardship(input) |
POST |
/portal/hardship/apply |
HardshipApplication |
React Query Hooks
Each hook file wraps portal API functions in React Query useQuery and useMutation hooks.
Query Key Structure
| Hook |
Query Key |
Stale Time |
useDashboard() |
["portal", "dashboard"] |
default |
useLoans() |
["portal", "loans"] |
default |
useLoan(id) |
["portal", "loans", id] |
default |
useAmortization(id) |
["portal", "loans", id, "amortization"] |
default |
usePayments(id) |
["portal", "loans", id, "payments"] |
default |
useInstruments() |
["portal", "instruments"] |
default |
useAutopay(id) |
["portal", "loans", id, "autopay"] |
default |
useDocuments(type?) |
["portal", "documents", type] |
default |
useProfileDetail() |
["portal", "profile"] |
default |
useCases() |
["portal", "cases"] |
default |
useCase(id) |
["portal", "cases", id] |
default |
useHardshipApplications() |
["portal", "hardship"] |
default |
useHardshipApplication(id) |
["portal", "hardship", id] |
default |
useBranding() |
["portal", "branding"] |
5 minutes |
Mutation Patterns
Mutations invalidate related queries on success:
// useSubmitPayment invalidates:
// - ["portal", "loans", loanId, "payments"] (payment list)
// - ["portal", "loans", loanId] (loan balance)
// - ["portal", "dashboard"] (dashboard totals)
// useAddInstrument invalidates:
// - ["portal", "instruments"]
// useUpdateProfile invalidates:
// - ["portal", "profile"]
// - auth context (borrower name may change)
Hook Files
| File |
Hooks |
usePortalApi.ts |
useDashboard, useLoans, useLoan, useAmortization |
usePayments.ts |
usePayments, usePreviewPayment, useSubmitPayment |
useInstruments.ts |
useInstruments, useAddInstrument, useDeleteInstrument |
useAutopay.ts |
useAutopay, useEnrollAutopay, useCancelAutopay, useEnableAutopay |
useDocuments.ts |
useDocuments, useDownloadUrl, useUploadDocument |
useProfile.ts |
useProfileDetail, useUpdateProfile, useUpdateCommPreferences |
useCases.ts |
useCases, useCase, useCreateCase, useAddCaseMessage |
useHardship.ts |
useHardshipApplications, useHardshipApplication, useSubmitHardship |
useBranding.ts |
useBranding |
Document Upload Hook
useUploadDocument() handles the three-step upload flow:
- Request presigned URL via
requestUploadLink()
- Upload file directly to the presigned S3 URL via
PUT
- Confirm upload via
confirmUpload()
- Invalidates
["portal", "documents"] on success
See Also