API Reference
One endpoint. One PDF. Complete financial intelligence.
Send a bank statement. Receive a structured affordability report in under two seconds. Everything you need to integrate Sorae is on this page.
Base URL
https://sorae.co.za
Endpoint
POST /api/v1/analyze
Auth
Bearer token
Content type
multipart/form-data
Authentication
API keys.
Every request must include a valid API key in the Authorization header. Keys are created in the dashboard and shown in full exactly once — store yours securely.
Key format
Live keys are prefixed sk_live_. Test keys are prefixed sk_test_. Test keys process real PDFs but do not deduct credits.
Key security
Never expose API keys in client-side code or public repositories. All keys are hashed server-side — Sorae cannot recover a lost key. Rotate from the dashboard if a key is compromised.
Authorization header
Authorization: Bearer sk_live_your_api_key_hereThe endpoint
POST /api/v1/analyze
Upload a PDF bank statement as multipart/form-data. Sorae extracts, parses, and analyses it — returning the full intelligence stack in a single response.
Request headers
| Field | Type | Description |
|---|---|---|
| Authorization | string required | Bearer sk_live_… — your API key. |
| x-client-reference | string optional | Your own reference ID (loan ID, application number, etc). Appears in logs and is stored with the report for reconciliation. |
Request body — multipart/form-data
| Field | Type | Description |
|---|---|---|
| file | File required | A PDF bank statement. Maximum 10 MB. Must be a branch-issued or internet banking PDF — not a screenshot or scanned image. |
Supported banks
Code examples
cURL
curl -X POST https://sorae.co.za/api/v1/analyze \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "x-client-reference: APP-00123" \
-F "file=@/path/to/statement.pdf"Node.js — fetch
import fs from 'fs'
const form = new FormData()
form.append('file', new Blob([fs.readFileSync('./statement.pdf')], { type: 'application/pdf' }), 'statement.pdf')
const res = await fetch('https://sorae.co.za/api/v1/analyze', {
method: 'POST',
headers: {
Authorization: 'Bearer sk_live_your_api_key',
'x-client-reference': 'APP-00123',
},
body: form,
})
const { success, data, error } = await res.json()
if (!success) {
console.error(error.code, error.message)
} else {
console.log('Grade:', data.affordability.grade)
console.log('Max instalment:', data.affordability.recommended_max_installment)
}Python — requests
import requests
with open('statement.pdf', 'rb') as f:
response = requests.post(
'https://sorae.co.za/api/v1/analyze',
headers={
'Authorization': 'Bearer sk_live_your_api_key',
'x-client-reference': 'APP-00123',
},
files={'file': ('statement.pdf', f, 'application/pdf')},
)
result = response.json()
if result['success']:
data = result['data']
print(f"Grade: {data['affordability']['grade']}")
print(f"Max instalment: R{data['affordability']['recommended_max_installment']:,.2f}")
else:
print(result['error']['code'], result['error']['message'])Response schema
The full result object.
Successful responses return HTTP 200 with this envelope. Failed calls return a non-2xx status with an error object — see Error codes below.
Response headers
X-Call-IDUUID for this request. Include in support queries.
X-Processing-MsServer-side processing time in milliseconds.
Success envelope
Success — HTTP 200
{
"success": true,
"data": { ... } // AnalysisResult — see fields below
}Top-level fields
| Field | Type | Description |
|---|---|---|
| call_id | string | Unique UUID for this analysis. Matches the X-Call-ID header. |
| bank | string | Detected bank. One of: CAPITEC, STANDARD_BANK, FNB, ABSA, NEDBANK. |
| statement_period | object | { from: "YYYY-MM-DD", to: "YYYY-MM-DD" } — the date range of the statement. |
| income | object | Full income analysis. See Income fields. |
| expenses | object | Categorised expense breakdown. See Expense fields. |
| balance_behaviour | object | End-of-month and overdraft behaviour signals. |
| affordability | object | Score, grade, disposable income, and recommended max instalment. |
| risk_flags | array | Array of detected risk signals. Empty array if none. |
| summary | string | Plain-language analyst summary for human readers. |
| processed_at | string | ISO 8601 timestamp of when this result was generated. |
| processing_ms | number | Server processing time in milliseconds. |
income
| Field | Type | Description |
|---|---|---|
| verified | boolean | Whether income could be confidently verified from transaction data. |
| type | string | SALARY | WAGES | IRREGULAR | MIXED | UNKNOWN. |
| monthly_net | number | Verified average net monthly income in ZAR. |
| salary_date | number|null | Day of month income typically arrives (1–31), or null if irregular. |
| consistency_score | number | Income consistency 0–100. Higher is more reliable. |
| months_analyzed | number | Number of calendar months covered by the statement. |
| income_sources | array | Array of identified income sources with description, frequency, and average amount. |
| volatility | object | Coefficient of variation, STABLE | MODERATE | VOLATILE | HIGHLY_VOLATILE classification, and monthly breakdown. |
| deposit_classification | object | Primary source type (EMPLOYER_PAYROLL, INFORMAL_EFT, etc.) with ratios. |
expenses
| Field | Type | Description |
|---|---|---|
| total_monthly | number | Average total monthly outflows in ZAR. |
| breakdown | object | Categorised monthly averages: rent_housing, debt_repayments, groceries_food, transport, subscriptions, gambling, cash_withdrawals, utilities, insurance, other. |
| rigidity | object | Fixed vs flexible expense split, fixed_ratio (0–1), and flexibility_score (0–100). |
balance_behaviour
| Field | Type | Description |
|---|---|---|
| average_end_of_month_balance | number | Average closing balance at month end in ZAR. |
| lowest_pre_payday_balance | number | Lowest balance in the 3 days before salary credit. |
| paycheck_to_paycheck | boolean | True if end-of-month balance is consistently below 5% of monthly income. |
| overdraft_frequency | number | Number of times the account went negative. |
| overdraft_depth_avg | number | Average overdraft depth in ZAR when the account was negative. |
| buffer_score | number | Cash buffer health 0–100. Higher is better. |
| months_with_negative | number | Count of calendar months where the balance went negative at least once. |
affordability
| Field | Type | Description |
|---|---|---|
| score | number | Composite affordability score 0–100. |
| grade | string | A | B | C | D | F — overall affordability grade. |
| disposable_income | number | Monthly net income minus total expenses in ZAR. |
| recommended_max_installment | number | Maximum recommended monthly loan instalment in ZAR. NCR-aligned. |
| debt_to_income_ratio | number | Total debt repayments as a fraction of income (0–1). |
risk_flags[ ]
| Field | Type | Description |
|---|---|---|
| code | string | Flag identifier. See full list of flag codes below. |
| severity | string | LOW | MEDIUM | HIGH | CRITICAL. |
| description | string | Human-readable explanation of the flag. |
| occurrences | number | Number of times this signal was detected in the period. |
| amount | number|undefined | Associated monetary value where applicable (e.g. total gambling spend). |
Example response
HTTP 200 — abbreviated
{
"success": true,
"data": {
"call_id": "3f4a1c2e-8b7d-4e1a-9f0c-1234567890ab",
"bank": "CAPITEC",
"statement_period": { "from": "2024-10-01", "to": "2025-01-31" },
"income": {
"verified": true,
"type": "SALARY",
"monthly_net": 28500,
"salary_date": 25,
"consistency_score": 91,
"months_analyzed": 4,
"volatility": {
"coefficient_of_variation": 4.2,
"classification": "STABLE"
}
},
"expenses": {
"total_monthly": 19800,
"breakdown": {
"rent_housing": 8000,
"debt_repayments": 4200,
"groceries_food": 3100,
"transport": 1800,
"subscriptions": 650,
"gambling": 0,
"cash_withdrawals": 1200,
"utilities": 580,
"insurance": 270,
"other": 0
}
},
"affordability": {
"score": 74,
"grade": "B",
"disposable_income": 8700,
"recommended_max_installment": 4350,
"debt_to_income_ratio": 0.15
},
"risk_flags": [
{
"code": "PRE_PAYDAY_CASH",
"severity": "MEDIUM",
"description": "Cash withdrawals in 3 days before salary credit.",
"occurrences": 3,
"amount": 2100
}
],
"summary": "Stable salaried income of R28,500/month verified across 4 months. Affordability grade B with R4,350 recommended maximum instalment. One medium-severity flag: pre-payday cash withdrawal pattern detected.",
"processed_at": "2025-01-31T09:14:22.411Z",
"processing_ms": 1284
}
}Risk flag codes
16 behavioural signals.
| Code | Severity | Description |
|---|---|---|
| RETURNED_DEBIT | HIGH | One or more debit orders were returned unpaid. |
| PRE_PAYDAY_CASH | MEDIUM | Cash withdrawals in the 3 days before salary credit. |
| GAMBLING_DETECTED | HIGH | Gambling transactions identified. |
| IRREGULAR_INCOME | MEDIUM | Income pattern is inconsistent month-to-month. |
| NO_INCOME_DETECTED | CRITICAL | No verifiable income found in the statement. |
| BALANCE_NEGATIVE | HIGH | Account balance went negative during the period. |
| MULTIPLE_LOANS | HIGH | Multiple active loan repayments detected. |
| INCOME_UNVERIFIED | MEDIUM | Income present but could not be confidently verified. |
| INCOME_VOLATILITY | MEDIUM | High coefficient of variation in monthly income. |
| PAYDAY_LOAN_DETECTED | HIGH | Short-term / payday loan activity found. |
| OVERDRAFT_RELIANCE | HIGH | Account frequently operates in overdraft. |
| LIFESTYLE_INFLATION | LOW | Discretionary spending increasing relative to income. |
| TRANSACTION_TIMING_STRESS | MEDIUM | Payment timing patterns suggest cash flow stress. |
| HIDDEN_DEBT_SIGNALS | HIGH | Indirect signals of undisclosed debt obligations. |
| CASH_WITHDRAWAL_HIGH | MEDIUM | Unusually high proportion of cash withdrawals. |
| PAYCHECK_TO_PAYCHECK | HIGH | End-of-month balance consistently below 5% of income. |
Error codes
Error responses.
All errors share the same envelope. Use error.code for programmatic handling and error.call_id when contacting support.
Error envelope
{
"success": false,
"error": {
"code": "PAYMENT_REQUIRED",
"message": "Insufficient credit balance. Top up at https://sorae.co.za/dashboard",
"call_id": "3f4a1c2e-8b7d-4e1a-9f0c-1234567890ab"
}
}| Code | Status | Description |
|---|---|---|
| UNAUTHORIZED | 401 | Missing or invalid API key. |
| FORBIDDEN | 403 | Valid key but insufficient permissions. |
| RATE_LIMITED | 429 | Too many requests. Check Retry-After header. |
| PAYMENT_REQUIRED | 402 | Insufficient credit balance. Top up in the dashboard. |
| INVALID_REQUEST | 400 | Malformed request — missing file or wrong content type. |
| FILE_TOO_LARGE | 422 | PDF exceeds the 10 MB limit. |
| UNSUPPORTED_FORMAT | 422 | Uploaded file is not a PDF. |
| UNSUPPORTED_BANK | 422 | Bank not yet supported. Check the supported banks list. |
| PARSE_FAILED | 422 | Could not extract usable data from the document. |
| ANALYSIS_FAILED | 500 | AI summary generation failed. |
| INTERNAL_ERROR | 500 | Unexpected server error. Retry with exponential backoff. |
Rate limit headers
On 429 RATE_LIMITED responses, check the Retry-After header (seconds to wait), X-RateLimit-Remaining-RPM, and X-RateLimit-Remaining-Daily to manage your retry logic.
Credits & rate limits
What gets charged. What gets throttled.
One credit per success
Credits are deducted only on HTTP 200 responses. Requests that fail at auth, parsing, or rate limiting are never charged.
Zero balance = 402
Requests made with no remaining credits return PAYMENT_REQUIRED. Top up from the dashboard to resume immediately.
Rate limits per key
Each API key has per-minute and daily request limits. Default limits are shown in the dashboard. Contact sales for higher throughput requirements.
POPIA & zero data retention
Original PDF files and raw transaction text are never persisted. All extraction and analysis happens in-memory. Once a response is returned, the document is discarded. Only non-PII metadata — bank name, statement period, processing time — is logged for audit purposes. Your clients' financial data stays yours.
Ready to integrate
Get your API key in two minutes.
Create a free account, generate a key from the dashboard, and run your first analysis. Three free credits included — no credit card required.