Skip to main content

Get notified when a job finishes

Have Walker Street Data call your server when a job finishes, instead of polling its status.

Before you start​

  • An API key with the webhooks:subscribe scope to create subscriptions, plus whichever scopes you need to read results (see Authentication). The Node.js, Python and C# snippets read it from the WSD_API_KEY environment variable.
  • The sandbox base URL: https://sandbox.walkerstdata.com.au.
  • A publicly reachable HTTPS URL for your receiver. Plain http:// is rejected. While developing, a tunnelling tool can expose a local port over HTTPS.
  • A customerId (see Classify a customer's transactions).
  • The receivers use Express (Node.js 18+, saved as an .mjs file), Flask (Python) and an ASP.NET Core minimal API (.NET 8, created with dotnet new web). The other C# snippets are .NET 8 top-level programs using HttpClient and System.Text.Json.

1. Create the subscription​

Subscribe to job.completed and give the URL of your receiver. Pass customerIds to hear about specific customers only; an empty list makes a catch-all subscription for every customer. Each subscription covers one event type, so create a second one for job.failed if you want to hear about failures too.

curl -X POST "https://sandbox.walkerstdata.com.au/v1/webhooks/subscriptions" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"eventType": "job.completed",
"webhookUrl": "https://your-app.example.com/webhooks/wsd",
"customerIds": ["524ba9bc-06e7-417e-bd54-b99785f5194a"]
}'

A successful create returns 201 Created:

{
"data": {
"webhookSubscriptionId": "e82c5f1a-94d3-4b7e-a6c0-3d19f8b2e754",
"eventType": "job.completed",
"webhookUrl": "https://your-app.example.com/webhooks/wsd",
"secret": "k9Xa2p7Qc1d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0=",
"customerIds": ["524ba9bc-06e7-417e-bd54-b99785f5194a"],
"isCatchAll": false,
"dateCreated": "2026-09-25T04:12:33.517Z"
},
"message": null
}
Save the secret now

The secret is only returned when the subscription is created or its secret is rotated. Keep it with your other credentials; the receiver below reads it from WSD_WEBHOOK_SECRET.

2. Run a receiver that verifies each delivery​

Every delivery is a POST with a JSON body and an X-Cypher-Webhook-Signature header. The signature is sha256= followed by the lowercase hex HMAC-SHA256 of the raw request body, keyed with your secret decoded from Base64. Verify it over the exact bytes you received (parsing and re-serialising the JSON will break the match), compare in constant time, and reject anything that doesn't match.

Respond with a 2xx within 30 seconds, and do any slow work after acknowledging. Deliveries can arrive more than once, so deduplicate on eventId. The receiver is server code, so there's no cURL version; step 3 tests it with cURL.

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = Buffer.from(process.env.WSD_WEBHOOK_SECRET, 'base64');
const seenEventIds = new Set(); // use a durable store in production

function signatureIsValid(rawBody, header) {
const expected =
'sha256=' +
crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex');
const a = Buffer.from(header ?? '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// express.raw keeps the body as the exact bytes received.
app.post(
'/webhooks/wsd',
express.raw({ type: 'application/json' }),
(req, res) => {
if (!signatureIsValid(req.body, req.get('X-Cypher-Webhook-Signature'))) {
return res.sendStatus(401);
}

const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(204); // acknowledge first, then process

if (!seenEventIds.has(event.eventId)) {
seenEventIds.add(event.eventId);
handleEvent(event).catch(console.error); // see step 4
}
}
);

async function handleEvent(event) {
console.log(event.eventType, event.jobId);
}

app.listen(8000);

A job.completed delivery looks like this. Ignore fields you don't recognise (some internal fields, such as traceparent, may appear and are often null), as new ones can be added.

{
"eventType": "job.completed",
"eventId": "e7c9a1b2-3d4e-5f60-7182-93a4b5c6d7e8",
"clientId": "a0ced7e1-da3a-40ea-a18f-4c829f816c6a",
"customerId": "524ba9bc-06e7-417e-bd54-b99785f5194a",
"timestamp": "2026-09-25T02:15:30Z",
"jobId": "3f9a6c2e-81d4-4b7a-a5e0-c7d219f4b863",
"abn": "53004085616",
"jobType": "Enrichment",
"totalProcessedTransactions": 398,
"totalReceivedTransactions": 412
}

3. Test the receiver locally​

Before waiting on a real job, sign a sample body yourself and send it to your receiver. This uses the same algorithm as the platform, so a 204 means your verification is right, and changing a single character of the body should get a 401.

BODY='{"eventType":"job.completed","eventId":"e7c9a1b2-3d4e-5f60-7182-93a4b5c6d7e8","customerId":"524ba9bc-06e7-417e-bd54-b99785f5194a","jobId":"3f9a6c2e-81d4-4b7a-a5e0-c7d219f4b863"}'
SECRET_HEX=$(printf '%s' "$WSD_WEBHOOK_SECRET" | base64 --decode | xxd -p | tr -d '\n')
SIGNATURE="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$SECRET_HEX" | sed 's/^.*= //')"

curl -i -X POST "http://localhost:8000/webhooks/wsd" \
-H "Content-Type: application/json" \
-H "X-Cypher-Webhook-Signature: $SIGNATURE" \
-d "$BODY"

Then submit transactions for the customer (see Classify a customer's transactions) and watch for the real delivery.

4. Fetch the results​

job.completed is sent once a job has finished with results, so its status is Completed or CompletedWithErrors. Replace the handleEvent / handle_event / HandleEvent stub from step 2: read the job status for the counts and any warnings, then fetch the job's transactions using the customerId and jobId from the payload.

curl "https://sandbox.walkerstdata.com.au/v1/jobs/3f9a6c2e-81d4-4b7a-a5e0-c7d219f4b863/status" \
-H "x-api-key: YOUR_API_KEY"

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"
Keep a fallback poll

Failed deliveries (a non-2xx response, a timeout or an unreachable URL) are not currently retried. Treat webhooks as the fast path and occasionally reconcile outstanding jobs with GET /v1/jobs or the job status endpoint. Report refresh jobs don't send job.completed at all: poll those (see Get a customer's reports and a lending decision).

What's next​