Get a customer's reports and a lending decision
Turn a customer's classified transactions into reports and a lending decision: a proceed, conditional or blocked verdict, with any computed limits.
Before you start
- An API key with these scopes (see Authentication):
customer:riskscoreto refresh reports,customer:reportsto read them, anddecision-policies:readanddecision-policies:evaluatefor decision policies. The Node.js, Python and C# snippets read the key from theWSD_API_KEYenvironment variable. - The sandbox base URL:
https://sandbox.walkerstdata.com.au. - A customer with enriched transactions (see Classify a customer's transactions or Upload PDF bank statements).
- At least one decision policy set up for your client. Decision policies are enabled per client: if the decision policy endpoints return
404, contact us. - Node.js snippets use the built-in
fetch(Node 18+) and top-levelawait, so save them as an.mjsfile. Python snippets userequests. C# snippets are .NET 8 top-level programs usingHttpClientandSystem.Text.Json, with no extra packages.
1. Refresh the reports
Reports aren't computed automatically when transactions arrive. Once the customer's submission jobs have finished, start a refresh to compute the reports over all of the customer's enriched transactions. It returns 202 Accepted with a job to track. Start another refresh whenever you submit more transactions and want the reports to include them.
- cURL
- Node.js
- Python
- C#
curl -X POST "https://sandbox.walkerstdata.com.au/v1/customer/524ba9bc-06e7-417e-bd54-b99785f5194a/reports/refresh" \
-H "x-api-key: YOUR_API_KEY"
const BASE_URL = 'https://sandbox.walkerstdata.com.au';
const HEADERS = {
'x-api-key': process.env.WSD_API_KEY,
'Content-Type': 'application/json',
};
const customerId = '524ba9bc-06e7-417e-bd54-b99785f5194a';
const refresh = await fetch(
`${BASE_URL}/v1/customer/${customerId}/reports/refresh`,
{ method: 'POST', headers: HEADERS }
);
if (!refresh.ok) throw new Error(`Refresh failed: ${refresh.status}`);
const jobId = (await refresh.json()).data.jobId;
import os
import time
import requests
BASE_URL = "https://sandbox.walkerstdata.com.au"
HEADERS = {"x-api-key": os.environ["WSD_API_KEY"]}
customer_id = "524ba9bc-06e7-417e-bd54-b99785f5194a"
resp = requests.post(f"{BASE_URL}/v1/customer/{customer_id}/reports/refresh", headers=HEADERS)
resp.raise_for_status()
job_id = resp.json()["data"]["jobId"]
using System.Net.Http.Json;
using System.Text.Json.Nodes;
var http = new HttpClient { BaseAddress = new Uri("https://sandbox.walkerstdata.com.au") };
http.DefaultRequestHeaders.Add("x-api-key", Environment.GetEnvironmentVariable("WSD_API_KEY"));
var customerId = "524ba9bc-06e7-417e-bd54-b99785f5194a";
var refresh = await http.PostAsync($"/v1/customer/{customerId}/reports/refresh", null);
refresh.EnsureSuccessStatusCode();
var jobId = (string)(await refresh.Content.ReadFromJsonAsync<JsonNode>())!["data"]!["jobId"]!;
{
"data": {
"jobId": "6b1c2d3e-4f50-6178-9abc-def012345678",
"flow": "ReportRefresh"
},
"message": null
}
A 409 means a job is already running for this customer: wait for it to finish, then try again. A 422 means the customer has no enriched transactions to report on yet.
2. Wait for the refresh to finish
Poll the refresh job until it reaches Completed, CompletedWithErrors or Failed. Report refresh jobs don't send a job.completed webhook, so polling is the way to track them.
- cURL
- Node.js
- Python
- C#
curl "https://sandbox.walkerstdata.com.au/v1/jobs/6b1c2d3e-4f50-6178-9abc-def012345678/status" \
-H "x-api-key: YOUR_API_KEY"
const TERMINAL = ['Completed', 'CompletedWithErrors', 'Failed'];
while (true) {
const r = await fetch(`${BASE_URL}/v1/jobs/${jobId}/status`, {
headers: HEADERS,
});
if (!r.ok) throw new Error(`Job status failed: ${r.status}`);
if (TERMINAL.includes((await r.json()).data.status)) break;
await new Promise((resolve) => setTimeout(resolve, 5000));
}
TERMINAL = {"Completed", "CompletedWithErrors", "Failed"}
while True:
resp = requests.get(f"{BASE_URL}/v1/jobs/{job_id}/status", headers=HEADERS)
resp.raise_for_status()
if resp.json()["data"]["status"] in TERMINAL:
break
time.sleep(5)
string[] terminal = ["Completed", "CompletedWithErrors", "Failed"];
while (true)
{
var job = (await http.GetFromJsonAsync<JsonNode>($"/v1/jobs/{jobId}/status"))!["data"]!;
if (terminal.Contains((string?)job["status"])) break;
await Task.Delay(TimeSpan.FromSeconds(5));
}
3. Read the report bundle
One call returns all three reports: completeness, risk and aggregate. Check each report's status before reading its data:
status | Meaning | data? |
|---|---|---|
pending | Not computed yet | No (null) |
ready | Reflects all of the customer's transactions | Yes |
stale | New transactions have arrived since it was computed, or its latest refresh failed | Yes (last good) |
failed: true means the most recent compute attempt errored; any data returned is the last good report.
- cURL
- Node.js
- Python
- C#
curl "https://sandbox.walkerstdata.com.au/v1/customer/524ba9bc-06e7-417e-bd54-b99785f5194a/reports" \
-H "x-api-key: YOUR_API_KEY"
const r = await fetch(`${BASE_URL}/v1/customer/${customerId}/reports`, {
headers: HEADERS,
});
if (!r.ok) throw new Error(`Get reports failed: ${r.status}`);
const reports = (await r.json()).data;
for (const kind of ['completeness', 'risk', 'aggregate']) {
const report = reports[kind];
console.log(
kind,
report.status,
report.failed ? 'failed' : '',
report.generatedAt
);
}
if (reports.risk.data) {
console.log(
'PD:',
reports.risk.data.value,
'rating:',
reports.risk.data.rating
);
}
resp = requests.get(f"{BASE_URL}/v1/customer/{customer_id}/reports", headers=HEADERS)
resp.raise_for_status()
reports = resp.json()["data"]
for kind in ("completeness", "risk", "aggregate"):
report = reports[kind]
print(kind, report["status"], "failed" if report.get("failed") else "", report["generatedAt"])
if reports["risk"]["data"]:
print("PD:", reports["risk"]["data"]["value"], "rating:", reports["risk"]["data"]["rating"])
var reports = (await http.GetFromJsonAsync<JsonNode>($"/v1/customer/{customerId}/reports"))!["data"]!;
foreach (var kind in new[] { "completeness", "risk", "aggregate" })
{
var report = reports[kind]!;
var failed = (bool?)report["failed"] == true ? "failed" : "";
Console.WriteLine($"{kind} {report["status"]} {failed} {report["generatedAt"]}");
}
var risk = reports["risk"]!["data"];
if (risk is not null)
{
Console.WriteLine($"PD: {risk["value"]} rating: {risk["rating"]}");
}
{
"data": {
"completeness": {
"status": "ready",
"failed": false,
"generatedAt": "2026-09-25T02:18:07Z",
"data": {
"summary": {
"completenessScore": 0.85,
"completenessTier": "high",
"totalMissingness": 0.15
}
}
},
"risk": {
"status": "ready",
"failed": false,
"generatedAt": "2026-09-25T02:18:07Z",
"data": {
"value": 9.54,
"rating": "B",
"pdConfidenceScore": 0.85,
"pdConfidenceRating": "High",
"errorCodes": []
}
},
"aggregate": {
"status": "stale",
"failed": true,
"generatedAt": "2026-09-18T01:02:44Z",
"data": {
"tables": [
{
"id": "income",
"name": "Income",
"hierarchy": "table",
"type": "objectArray",
"values": []
}
]
}
}
},
"message": null
}
Here completeness and risk are current, while the aggregate report's last refresh failed and the previous report is returned. See Completeness, Risk Score and Aggregate for every field.
4. Find the decision policy and its inputs
List your decision policies to get the slug of the one you want and its declaredInputs. Every declared input must be supplied when you evaluate, with a value of the declared kind (string, decimal or boolean).
- cURL
- Node.js
- Python
- C#
curl "https://sandbox.walkerstdata.com.au/v1/decision-policies?page=1&pageSize=20" \
-H "x-api-key: YOUR_API_KEY"
const list = await fetch(
`${BASE_URL}/v1/decision-policies?page=1&pageSize=20`,
{
headers: HEADERS,
}
);
if (!list.ok) throw new Error(`List policies failed: ${list.status}`);
for (const policy of (await list.json()).data.items) {
const inputs = policy.declaredInputs.map((i) => `${i.name} (${i.kind})`);
console.log(policy.slug, 'inputs:', inputs);
}
resp = requests.get(
f"{BASE_URL}/v1/decision-policies", headers=HEADERS, params={"page": 1, "pageSize": 20}
)
resp.raise_for_status()
for policy in resp.json()["data"]["items"]:
inputs = [f"{i['name']} ({i['kind']})" for i in policy["declaredInputs"]]
print(policy["slug"], "inputs:", inputs)
var policies = (await http.GetFromJsonAsync<JsonNode>(
"/v1/decision-policies?page=1&pageSize=20"))!["data"]!;
foreach (var policy in policies["items"]!.AsArray())
{
var inputs = policy!["declaredInputs"]!.AsArray().Select(i => $"{i!["name"]} ({i["kind"]})");
Console.WriteLine($"{policy["slug"]} inputs: {string.Join(", ", inputs)}");
}
{
"data": {
"items": [
{
"policyId": "b91c4e27-6d3f-4a85-9e12-7f0a5c8d3b61",
"name": "Business line of credit v1",
"slug": "business-line-of-credit-v1",
"notes": "Three times average monthly net inflow, blocked on recent dishonours.",
"declaredInputs": [{ "name": "requested_amount", "kind": "decimal" }],
"defaultOutcome": "proceed",
"isArchived": false
}
],
"page": 1,
"pageSize": 20,
"totalCount": 1
},
"message": null
}
Each policy also carries its steps (applied in order; the first step that matches decides the outcome), its limits and the metrics and products they refer to. Archived policies are left out unless you pass includeArchived=true.
5. Evaluate the policy
Post the policy's inputs to evaluate it for the customer. The body depends on the policy: this one declares a single requested_amount input, so that's the only key in inputs. Leaving out a declared input returns 400 with the code DecisionPolicy.MissingInputs, and an undeclared one returns DecisionPolicy.UnknownInputs.
- cURL
- Node.js
- Python
- C#
curl -X POST "https://sandbox.walkerstdata.com.au/v1/customer/524ba9bc-06e7-417e-bd54-b99785f5194a/decision-policies/business-line-of-credit-v1/evaluate" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "inputs": { "requested_amount": 50000 } }'
const evaluate = await fetch(
`${BASE_URL}/v1/customer/${customerId}/decision-policies/business-line-of-credit-v1/evaluate`,
{
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ inputs: { requested_amount: 50000 } }),
}
);
if (!evaluate.ok) throw new Error(`Evaluate failed: ${evaluate.status}`);
const result = (await evaluate.json()).data;
console.log(result.verdict.outcome, result.verdict.recommendedActions);
resp = requests.post(
f"{BASE_URL}/v1/customer/{customer_id}/decision-policies/business-line-of-credit-v1/evaluate",
headers=HEADERS,
json={"inputs": {"requested_amount": 50000}},
)
resp.raise_for_status()
result = resp.json()["data"]
print(result["verdict"]["outcome"], result["verdict"]["recommendedActions"])
var evaluate = await http.PostAsJsonAsync(
$"/v1/customer/{customerId}/decision-policies/business-line-of-credit-v1/evaluate",
new { inputs = new { requested_amount = 50000 } });
evaluate.EnsureSuccessStatusCode();
var result = (await evaluate.Content.ReadFromJsonAsync<JsonNode>())!["data"]!;
var verdict = result["verdict"]!;
Console.WriteLine($"{verdict["outcome"]} {verdict["recommendedActions"]!.ToJsonString()}");
{
"data": {
"verdict": {
"outcome": "conditional",
"recommendedActions": [
"Request the last three months of bank statements"
],
"reasons": []
},
"computedLimits": [
{
"kind": "computed",
"productId": "3f8d2a6c-91e4-4b7a-8d05-c6e3b1f92a47",
"amount": 150000,
"capped": true,
"uncappedAmount": 184640.25
}
],
"products": [
{
"productId": "3f8d2a6c-91e4-4b7a-8d05-c6e3b1f92a47",
"slug": "business-line-of-credit",
"name": "Business line of credit"
}
],
"indicators": [
{
"kind": "reading",
"metricId": "7c2e9f41-3b8a-4d6e-a15f-92d04b7e6c13",
"value": 2,
"format": "integer"
}
],
"metrics": [
{
"metricId": "7c2e9f41-3b8a-4d6e-a15f-92d04b7e6c13",
"name": "Dishonours last 90 days"
}
],
"attributes": [{ "name": "vedaScore", "value": 712.5, "source": "stored" }]
},
"message": null
}
6. Read the verdict
verdict.outcomeis the decision:proceed,conditionalorblocked, taken from the first step that matched (or the policy'sdefaultOutcomeif none did). It'sindeterminatewhen the policy couldn't be decided, for example because a metric couldn't be calculated from the customer's transactions;verdict.reasonsthen explains why, each reason with a stablecode(such asDecisionPolicy.MetricUnavailable), amessageyou can display and themetricIdsinvolved.verdict.recommendedActionslists the recommended action of every matched rule in the deciding step. Here, aconditionaloutcome asks for more statements before proceeding.computedLimitshas one entry per lending product the policy prices. Acomputedentry gives the limitamount; when a ceiling applied,cappedistrueanduncappedAmountshows the figure before the cap. Anindeterminateentry hasreasonsinstead of an amount. MatchproductIdagainstproductsfor the product's name and slug.indicatorsare the metric readings the policy publishes alongside its verdict (with aformatfor display), andmetricsgives each metric's name.attributesshows every customer attribute the decision read and whether the value wasstoredoroverridden.
To try a what-if without changing the customer's stored attributes, add attributeOverrides to the request, for example { "inputs": { "requested_amount": 50000 }, "attributeOverrides": { "vedaScore": 650 } }. To update the stored values, use Set customer attributes.
What's next
- Reports: how the report bundle is produced and refreshed
- Completeness, Risk Score and Aggregate: the fields inside each report
- Classify a customer's transactions: get transaction data in for a new customer
- API reference: Refresh customer reports, Get customer reports, List decision policies, Evaluate decision policy