Classify a customer's transactions
Send a business's bank transactions to Walker Street Data and get each one back categorised, with its counterparty and confidence scores.
Before you start
- An API key with the
transactions:enrichscope (see Authentication). The Node.js, Python and C# snippets read it from theWSD_API_KEYenvironment variable. - The sandbox base URL:
https://sandbox.walkerstdata.com.au. - The customer's 11-digit ABN and some transactions for one of their bank accounts.
- 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.
Each step builds on the one before it: variables such as customer_id and job_id carry over.
1. Create the customer
Create a customer from their ABN. The call is idempotent: if the ABN is already known, you get the existing customerId back, so it's safe to call every time.
- cURL
- Node.js
- Python
- C#
curl -X POST "https://sandbox.walkerstdata.com.au/v1/customer" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "abn": "53004085616" }'
const BASE_URL = 'https://sandbox.walkerstdata.com.au';
const HEADERS = {
'x-api-key': process.env.WSD_API_KEY,
'Content-Type': 'application/json',
};
const resp = await fetch(`${BASE_URL}/v1/customer`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ abn: '53004085616' }),
});
if (!resp.ok) throw new Error(`Create customer failed: ${resp.status}`);
const customerId = (await resp.json()).data.customerId;
import os
import time
import requests
BASE_URL = "https://sandbox.walkerstdata.com.au"
HEADERS = {"x-api-key": os.environ["WSD_API_KEY"]}
resp = requests.post(f"{BASE_URL}/v1/customer", headers=HEADERS, json={"abn": "53004085616"})
resp.raise_for_status()
customer_id = resp.json()["data"]["customerId"]
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 resp = await http.PostAsJsonAsync("/v1/customer", new { abn = "53004085616" });
resp.EnsureSuccessStatusCode();
var customerId = (string)(await resp.Content.ReadFromJsonAsync<JsonNode>())!["data"]!["customerId"]!;
{
"data": {
"customerId": "524ba9bc-06e7-417e-bd54-b99785f5194a",
"abn": "53004085616"
},
"message": null
}
2. Submit the transactions
Send one account's details plus its transactions. Amounts are signed: negative for money out, positive for money in. Add your own transactionId to each transaction if you have one; it comes back as suppliedId so you can match results to your records. See Submit transactions as JSON for every field and limit.
- cURL
- Node.js
- Python
- C#
curl -X POST "https://sandbox.walkerstdata.com.au/v1/customer/524ba9bc-06e7-417e-bd54-b99785f5194a/transactions" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"bankName": "Commonwealth Bank",
"bsb": "062-000",
"accountNumber": "10234567",
"accountName": "ACME Business Pty Ltd",
"accountType": "Business Transaction Account",
"transactions": [
{
"transactionId": "txn-20250409-0001",
"transactionDate": "2025-04-09",
"amount": 13167.00,
"description": "Direct Credit 158824 ARNOTT'\''S BISCUIT 339820002916682025",
"balance": 33155.05
},
{
"transactionId": "txn-20250408-0002",
"transactionDate": "2025-04-08",
"amount": -122.18,
"description": "METRO BLACKTOWN SOUTH BLACKTOWN AU Card xx3880 Value Date: 04/04/2025",
"balance": 19988.05
}
]
}'
const payload = {
bankName: 'Commonwealth Bank',
bsb: '062-000',
accountNumber: '10234567',
accountName: 'ACME Business Pty Ltd',
accountType: 'Business Transaction Account',
transactions: [
{
transactionId: 'txn-20250409-0001',
transactionDate: '2025-04-09',
amount: 13167.0,
description: "Direct Credit 158824 ARNOTT'S BISCUIT 339820002916682025",
balance: 33155.05,
},
{
transactionId: 'txn-20250408-0002',
transactionDate: '2025-04-08',
amount: -122.18,
description:
'METRO BLACKTOWN SOUTH BLACKTOWN AU Card xx3880 Value Date: 04/04/2025',
balance: 19988.05,
},
],
};
const submit = await fetch(
`${BASE_URL}/v1/customer/${customerId}/transactions`,
{ method: 'POST', headers: HEADERS, body: JSON.stringify(payload) }
);
if (!submit.ok) throw new Error(`Submit failed: ${submit.status}`);
const jobId = (await submit.json()).data.jobId;
payload = {
"bankName": "Commonwealth Bank",
"bsb": "062-000",
"accountNumber": "10234567",
"accountName": "ACME Business Pty Ltd",
"accountType": "Business Transaction Account",
"transactions": [
{
"transactionId": "txn-20250409-0001",
"transactionDate": "2025-04-09",
"amount": 13167.00,
"description": "Direct Credit 158824 ARNOTT'S BISCUIT 339820002916682025",
"balance": 33155.05,
},
{
"transactionId": "txn-20250408-0002",
"transactionDate": "2025-04-08",
"amount": -122.18,
"description": "METRO BLACKTOWN SOUTH BLACKTOWN AU Card xx3880 Value Date: 04/04/2025",
"balance": 19988.05,
},
],
}
resp = requests.post(
f"{BASE_URL}/v1/customer/{customer_id}/transactions", headers=HEADERS, json=payload
)
resp.raise_for_status()
job_id = resp.json()["data"]["jobId"]
var payload = new
{
bankName = "Commonwealth Bank",
bsb = "062-000",
accountNumber = "10234567",
accountName = "ACME Business Pty Ltd",
accountType = "Business Transaction Account",
transactions = new[]
{
new
{
transactionId = "txn-20250409-0001",
transactionDate = "2025-04-09",
amount = 13167.00m,
description = "Direct Credit 158824 ARNOTT'S BISCUIT 339820002916682025",
balance = 33155.05m,
},
new
{
transactionId = "txn-20250408-0002",
transactionDate = "2025-04-08",
amount = -122.18m,
description = "METRO BLACKTOWN SOUTH BLACKTOWN AU Card xx3880 Value Date: 04/04/2025",
balance = 19988.05m,
},
},
};
var submit = await http.PostAsJsonAsync($"/v1/customer/{customerId}/transactions", payload);
submit.EnsureSuccessStatusCode();
var jobId = (string)(await submit.Content.ReadFromJsonAsync<JsonNode>())!["data"]!["jobId"]!;
The API accepts the batch straight away and returns a job to track:
{
"data": {
"jobId": "3f9a6c2e-81d4-4b7a-a5e0-c7d219f4b863",
"status": "New",
"flow": "RawJson",
"totalReceivedTransactions": 2
},
"message": null
}
3. Wait for the job to finish
Poll the job's status until it reaches a terminal state: Completed, CompletedWithErrors or Failed. Don't wait for Completed alone, as a job that finishes CompletedWithErrors still has usable results. Every few seconds is plenty; to avoid polling altogether, use a webhook.
- cURL
- Node.js
- Python
- C#
curl "https://sandbox.walkerstdata.com.au/v1/jobs/3f9a6c2e-81d4-4b7a-a5e0-c7d219f4b863/status" \
-H "x-api-key: YOUR_API_KEY"
const TERMINAL = ['Completed', 'CompletedWithErrors', 'Failed'];
let job;
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}`);
job = (await r.json()).data;
if (TERMINAL.includes(job.status)) break;
await new Promise((resolve) => setTimeout(resolve, 5000));
}
if (job.status === 'Failed') throw new Error(`Job ${jobId} failed`);
TERMINAL = {"Completed", "CompletedWithErrors", "Failed"}
while True:
resp = requests.get(f"{BASE_URL}/v1/jobs/{job_id}/status", headers=HEADERS)
resp.raise_for_status()
job = resp.json()["data"]
if job["status"] in TERMINAL:
break
time.sleep(5)
if job["status"] == "Failed":
raise RuntimeError(f"Job {job_id} failed")
string[] terminal = ["Completed", "CompletedWithErrors", "Failed"];
JsonNode job;
while (true)
{
job = (await http.GetFromJsonAsync<JsonNode>($"/v1/jobs/{jobId}/status"))!["data"]!;
if (terminal.Contains((string?)job["status"])) break;
await Task.Delay(TimeSpan.FromSeconds(5));
}
if ((string?)job["status"] == "Failed") throw new Exception($"Job {jobId} failed");
{
"data": {
"jobId": "3f9a6c2e-81d4-4b7a-a5e0-c7d219f4b863",
"status": "Completed",
"flow": "RawJson",
"totalReceivedTransactions": 2,
"totalProcessedTransactions": 2,
"totalDuplicateTransactions": 0,
"warnings": null
},
"message": null
}
A non-zero totalDuplicateTransactions usually means the batch was sent before. That's safe: duplicates aren't stored twice, and they're still linked to this job. See Jobs for the full lifecycle.
4. Fetch the classified transactions
Read the customer's transactions, scoped to this job with jobId. Pass page and count to page through large results (see Pagination).
- cURL
- Node.js
- Python
- C#
curl "https://sandbox.walkerstdata.com.au/v1/customer/524ba9bc-06e7-417e-bd54-b99785f5194a/transactions?jobId=3f9a6c2e-81d4-4b7a-a5e0-c7d219f4b863&page=1&count=100" \
-H "x-api-key: YOUR_API_KEY"
const params = new URLSearchParams({ jobId, page: '1', count: '100' });
const txns = await fetch(
`${BASE_URL}/v1/customer/${customerId}/transactions?${params}`,
{ headers: HEADERS }
);
if (!txns.ok) throw new Error(`Get transactions failed: ${txns.status}`);
for (const txn of (await txns.json()).data.transactions) {
const e = txn.enrichment ?? {};
console.log(
txn.suppliedId,
txn.amount,
e.level1?.value,
e.level2?.value,
e.level3?.counterpartyName
);
}
resp = requests.get(
f"{BASE_URL}/v1/customer/{customer_id}/transactions",
headers=HEADERS,
params={"jobId": job_id, "page": 1, "count": 100},
)
resp.raise_for_status()
for txn in resp.json()["data"]["transactions"]:
enrichment = txn["enrichment"] or {}
level1 = (enrichment.get("level1") or {}).get("value")
level2 = (enrichment.get("level2") or {}).get("value")
counterparty = (enrichment.get("level3") or {}).get("counterpartyName")
print(txn["suppliedId"], txn["amount"], level1, level2, counterparty)
var txns = (await http.GetFromJsonAsync<JsonNode>(
$"/v1/customer/{customerId}/transactions?jobId={jobId}&page=1&count=100"))!["data"]!;
foreach (var txn in txns["transactions"]!.AsArray())
{
var e = txn!["enrichment"];
Console.WriteLine(string.Join(" ",
txn["suppliedId"], txn["amount"],
e?["level1"]?["value"], e?["level2"]?["value"], e?["level3"]?["counterpartyName"]));
}
Each transaction keeps its original values and gains an enrichment block: a Level 1 category, a Level 2 sub-category and the Level 3 counterparty, each with a confidence score. The account details are returned once in accounts and referenced by accountId.
{
"data": {
"accounts": [
{
"accountId": "b3f1d8e2-6a47-4c95-8e1b-0f2a9c7d5e63",
"bsb": "062-000",
"accountNumber": "10234567",
"institutionName": "Commonwealth Bank",
"sourcedVia": "raw"
}
],
"transactions": [
{
"transactionId": "e41c7a90-2b6d-4f85-9e13-7ac5d8b02f6e",
"suppliedId": "txn-20250409-0001",
"accountId": "b3f1d8e2-6a47-4c95-8e1b-0f2a9c7d5e63",
"transactionDate": "2025-04-09",
"amount": 13167.0,
"description": "Direct Credit 158824 ARNOTT'S BISCUIT 339820002916682025",
"balance": 33155.05,
"state": "Posted",
"enrichmentSource": "Cypher",
"enrichment": {
"level1": { "value": "credit_sales", "confidence": "0.94" },
"level2": { "value": "credit_sales_invoices", "confidence": "0.91" },
"level3": {
"counterpartyName": "Arnott's Biscuits Limited",
"counterpartyABN": "53004085616",
"confidence": "0.87"
}
}
}
],
"totalCount": 2,
"currentPage": 1,
"pageSize": 100,
"totalPages": 1
},
"message": null
}
enrichment is null for a transaction that hasn't been enriched. The full list of Level 1 and Level 2 tags is available from GET /v1/configuration/tags.
What's next
- Enriched transactions: the taxonomy, filters and sorting options in detail
- Get notified when a job finishes: replace polling with a webhook
- Get a customer's reports and a lending decision: turn the classified transactions into insight
- API reference: Create customer, Submit transactions, Get job status, Get customer transactions