The Veqtrx Partner API is a server-to-server affordability assessment service for banks, fintechs, and lenders. You send six months of transactions plus a customer’s declared circumstances; we return an FCA-compliant affordability assessment with risk and vulnerability flags.
https://veqtrx-staging-backend.onrender.com/api/partner/v1X-Partner-Key header (long-lived
API key, server-side only)pilot / standard /
enterprise).api_key along with an api_key_prefix
(e.g. prt_acmebank_). The key is shown exactly once — store
it securely.webhook_url. We return a
webhook_secret used to HMAC-sign every delivery (see
§7).Every authenticated request must include:
X-Partner-Key: prt_<your_slug>_<random>
We never accept partner keys from a browser; this API is meant for your backend systems only. Keys are bcrypt-hashed on our side — losing it means rotating it (we can’t recover it).
Request a rotation key via your account manager. The old key keeps working until you explicitly ask us to disable it, so you can cut over without downtime.
POST /assessRun an affordability assessment for one customer.
Request body (application/json):
| Field | Type | Description |
|---|---|---|
customer_ref |
string | Your internal customer ID (opaque to us). Required. |
partner_ref |
string | Optional reference for this specific assessment (e.g. loan-app ID). |
transactions |
array | 1 – 10,000 transactions. See schema below. |
circumstances |
object | Housing, household composition, vulnerabilities. |
declared_income |
number | Self-declared monthly income (GBP). Optional but recommended. |
callback_url |
string | Override the partner default webhook for this single call. |
Transaction schema:
| Field | Type | Description |
|---|---|---|
date |
string | ISO 8601 (YYYY-MM-DD). |
amount |
number | Positive = spending, negative = income/refund (Plaid convention). |
merchant_name |
string | Merchant or counterparty name. |
description |
string | Free-text raw description (optional). |
category_hint |
string | Plaid personal_finance_category.primary if you have
one. Skips our merchant-intel enrichment. |
account_id |
string | Source account; defaults to "default". |
Circumstances schema:
| Field | Type | Allowed |
|---|---|---|
housing |
string | renter, homeowner_mortgage,
homeowner_outright, social_housing |
household_adults |
integer | 1 – 10 |
children_u16 |
integer | 0 – 10 |
children_16_18 |
integer | 0 – 10 |
vulnerabilities |
array | Free-form codes from the FCA vulnerability taxonomy. |
Response (200 OK):
{
"assessment_id": "AF-A3F9B2C1D4E5",
"partner_ref": "loan-app-12345",
"customer_ref": "cust-001",
"created_at": "2026-05-13T12:34:56.789Z",
"monthly_income": 2200.0,
"monthly_expenditure": 1820.0,
"disposable_income": 380.0,
"affordable_amount": 380.0,
"affordability_score": 17,
"categories": {
"Food & Housekeeping": 540.0,
"Transport": 210.0
},
"fca_compliant": true,
"risk_flags": [],
"vulnerability_flags": [],
"transactions_analyzed": 412,
"confidence_score": 0.97,
"months_analyzed": 6.0,
"processing_ms": 91
}GET /assessment/{assessment_id}Retrieve a previously generated assessment. Returns 404 (not 403) if the ID belongs to a different partner.
GET /usageCurrent month usage and quota. Does not consume quota itself.
{
"partner_id": "acmebank",
"tier": "pilot",
"monthly_quota": 1000,
"current_usage": 412,
"remaining": 588,
"reset_date": "1st of next month"
}GET /healthUnauthenticated. Returns
{"status": "ok", "version": "1.0"}.
| Status | Meaning |
|---|---|
| 400 | Validation error (bad housing value, malformed transaction, etc.) |
| 401 | Missing / invalid / disabled API key |
| 404 | Assessment not found (or owned by a different partner) |
| 429 | Monthly quota exhausted |
| 500 | Engine error — try again. Persistent 500s should be escalated. |
All error responses follow
{"detail": "<message>"}.
/assess
call. Pilot tier is 1,000 calls/month; enterprise tiers
configurable.GET /usage and GET /assessment/{id} do not
count against quota.Every partner row is created with is_test_mode = true
and is moved to production after a sign-off call. Test-mode assessments
still run the real engine but are flagged in our admin dashboard.
If you configured a webhook_url, we POST the assessment
payload (same shape as the /assess response) to that URL
after each successful assessment. Delivery is fire-and-forget with 3
retries and exponential backoff (1s, 2s, 4s).
Headers we send:
Content-Type: application/json
X-Veqtrx-Signature: sha256=<hex>
X-Veqtrx-Timestamp: <unix-seconds>
User-Agent: Veqtrx-Webhooks/1.0
Verifying the signature (Python):
import hmac, hashlib
expected = "sha256=" + hmac.new(
webhook_secret.encode(),
raw_request_body, # bytes, *exact* body — do not re-serialise
hashlib.sha256,
).hexdigest()
assert hmac.compare_digest(expected, request.headers["X-Veqtrx-Signature"])Reject any request where the signature doesn’t match, or where the timestamp is older than your tolerance window (we recommend 5 minutes).
category_hint. We skip
our merchant-intel enrichment, which is the slow path.assessment_id on your side.
That’s how you re-fetch via /assessment/{id} and how we
cross-reference issues in support.webhook_secret only on request, so the same secret protects
all deliveries.requests)import requests
resp = requests.post(
"https://veqtrx-staging-backend.onrender.com/api/partner/v1/assess",
headers={"X-Partner-Key": os.environ["VEQTRX_PARTNER_KEY"]},
json={
"customer_ref": "cust-001",
"partner_ref": "loan-app-12345",
"transactions": transactions,
"circumstances": {
"housing": "renter",
"household_adults": 1,
"children_u16": 0,
"children_16_18": 0,
"vulnerabilities": [],
},
"declared_income": 2200.0,
},
timeout=10.0,
)
resp.raise_for_status()
assessment = resp.json()fetch)const resp = await fetch(
"https://veqtrx-staging-backend.onrender.com/api/partner/v1/assess",
{
method: "POST",
headers: {
"X-Partner-Key": process.env.VEQTRX_PARTNER_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
customer_ref: "cust-001",
partner_ref: "loan-app-12345",
transactions,
circumstances: {
housing: "renter",
household_adults: 1,
children_u16: 0,
children_16_18: 0,
vulnerabilities: [],
},
declared_income: 2200.0,
}),
},
);
if (!resp.ok) throw new Error(await resp.text());
const assessment = await resp.json();Architecture B is for partners who’d rather have us host the customer-facing flow than build it themselves. Your servers never touch the customer’s bank credentials; we operate the journey on our infrastructure under your brand and notify you via webhook when the assessment is complete.
| Architecture A (Direct API) | Architecture B (Portal) | |
|---|---|---|
| Bank holds raw transactions | Yes — you POST them | No — Veqtrx fetches via Plaid |
| Bank builds the UI | Yes | No — we host it under your brand |
| Customer signup needed | No | Yes (lightweight) |
| Result delivery | Synchronous JSON | Webhook + portal redirect |
You can use both for the same partner: provision once, use either flow per customer.
Ask us to enable portal mode on your partner record. You’ll need to supply:
primary_color, optional
accent_color, optional logo_url,
display_name) — applied as CSS variables on the portal
page.hide_veqtrx_branding flag — when set, the “Powered by
Veqtrx” footer is hidden.Admins flip these via
PUT /api/admin/partners/{partner_id}/portal. Once
portal_enabled=true, you can mint links.
curl -X POST https://veqtrx-backend.onrender.com/api/admin/partners/<id>/portal/links \
-H "Authorization: Bearer <admin_jwt>" \
-H "Content-Type: application/json" \
-d '{
"customer_ref": "cust-001",
"prefilled_name": "Alex Customer",
"prefilled_email": "alex@example.com",
"redirect_url": "https://bank.example.com/return",
"ttl_hours": 24
}'Response (returned exactly once):
{
"session_id": "uuid",
"partner_id": "yourbank",
"url": "https://affordiffy.co.uk/p/yourbank?t=<token>",
"token": "<token>",
"expires_at": "2026-05-14T12:00:00Z",
"important": "Save the URL or token now — it will not be shown again."
}The token is bcrypt-hashed on our side; we cannot replay it for you if it’s lost. Mint a new link instead.
Drop them into the url above (SMS, email, dashboard
handoff — your call). They land on a branded landing page that:
When the customer finishes, we:
completed (the token cannot be
re-used).BudgetReport to your partner
row.{
"event": "portal.assessment_completed",
"partner_id": "yourbank",
"session_id": "<session_uuid>",
"customer_ref": "cust-001",
"assessment_id": "AF-A3F9B2C1D4E5",
"completed_at": "2026-05-13T14:21:33Z"
}Same HMAC signing as Architecture A — verify
X-Veqtrx-Signature before trusting the payload.
redirect_url you supplied
at link creation (if any). If you didn’t supply one, they’re sent to a
generic “thanks” page.Once you have an assessment_id from the webhook, fetch
it via the Architecture A endpoint with your partner API key:
GET /api/partner/v1/assessment/{assessment_id}
X-Partner-Key: <your_key>
You get the same AssessmentResponse shape Architecture A
produces.
ttl_hours: 720). Mint shorter when the customer is
actively in the flow./p/<unknown-slug> returns the same 404 body as
probing a known slug with a bad token. You don’t leak which partners
exist.redirect_url is validated against your
allowed_redirect_domains allowlist at link-creation time.
We refuse private IPs, cloud metadata endpoints, and non-HTTP(S) schemes
unconditionally.portal_sessions table. A DB leak does NOT expose
live session URLs.