Webhooks
Webhooks let Walker Street Data notify your application when an event occurs - for example, when a job finishes enriching - so you don't have to poll for status. We POST a JSON payload to a URL you register, signed so you can verify it came from us.
Events
Subscribe to one event type per subscription:
| Event type | Fires when |
|---|---|
job.completed | A job finishes successfully (status Completed) |
job.failed | A job fails outright (status Failed) |
job.extraction.completed | The PDF extraction stage of a job finishes |
job.extraction.failed | The PDF extraction stage of a job fails |
job.enrichment.completed | The enrichment stage of a job finishes |
job.enrichment.failed | The enrichment stage of a job fails |
monitoring.rule.triggered | An in-life monitoring rule matches for a customer |
Create a subscription
Register a subscription with the event type you want to receive and the URL we should call. The URL must be an absolute HTTPS URL - plain http:// is rejected with 400. A successful create returns 201 Created.
curl -X POST "https://api.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": []
}'
The response includes the subscription ID and a signing secret. The secret is a Base64-encoded string and is only returned on create and on rotation - store it securely now, as it can't be retrieved later.
{
"data": {
"webhookSubscriptionId": "b3c1e2d4-5f6a-7890-abcd-ef1234567890",
"eventType": "job.completed",
"webhookUrl": "https://your-app.example.com/webhooks/wsd",
"secret": "k9Xa2p7Qc1d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0=",
"customerIds": [],
"isCatchAll": true,
"dateCreated": "2026-07-02T00:00:00Z"
},
"message": null
}
The webhook payload
We POST a JSON body with Content-Type: application/json. Every payload carries a common envelope:
| Field | Type | Description |
|---|---|---|
eventType | string | The event type (matches your subscription) |
eventId | uuid | Unique ID for this event - use it to deduplicate |
clientId | uuid | Your client ID |
customerId | uuid | The customer the event relates to |
timestamp | string | ISO 8601 time the event was generated |
Payloads can also carry internal fields (such as traceparent, tracestate, and tenantId, often null). Ignore fields you don't recognise - new ones may be added without notice - and don't fail parsing on unknown or null fields.
Job events (job.*) add jobId, abn, and jobType, plus event-specific fields:
| Event type | Additional fields |
|---|---|
job.completed | totalProcessedTransactions, totalReceivedTransactions |
job.extraction.completed | extractedCount, duplicateCount |
job.enrichment.completed | enrichedCount, duplicateCount |
job.failed, job.extraction.failed, job.enrichment.failed | error (nullable) |
monitoring.rule.triggered | evaluationResultId, monitoringRuleId, ruleName, runDate, interpretation[], recommendedAction[] (no jobId/abn/jobType) |
Example job.completed delivery:
{
"eventType": "job.completed",
"eventId": "e7c9a1b2-3d4e-5f60-7182-93a4b5c6d7e8",
"clientId": "a0ced7e1-da3a-40ea-a18f-4c829f816c6a",
"customerId": "524ba9bc-06e7-417e-bd54-b99785f5194a",
"timestamp": "2026-07-08T02:15:30Z",
"jobId": "34e1bbd4-36be-4d95-9351-a047eed5bf97",
"abn": "12345678901",
"jobType": "Enrichment",
"totalProcessedTransactions": 118,
"totalReceivedTransactions": 120
}
Verifying deliveries
Every delivery includes an X-Cypher-Webhook-Signature header. Verify it before acting on the payload, and reject anything that doesn't match.
The signature is sha256= followed by the lowercase hex HMAC-SHA256 of the raw request body, keyed by your subscription secret decoded from Base64:
X-Cypher-Webhook-Signature: sha256=3b2a1f...<hex>
Compute the expected value over the exact bytes you received (do not re-serialize the JSON - whitespace differences will break the match) and compare in constant time. Node.js example:
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secretBase64) {
const key = Buffer.from(secretBase64, 'base64'); // secret is Base64 - decode to raw key bytes
const expected =
'sha256=' + crypto.createHmac('sha256', key).update(rawBody).digest('hex');
const received = Buffer.from(signatureHeader ?? '');
const computed = Buffer.from(expected);
return (
received.length === computed.length &&
crypto.timingSafeEqual(received, computed)
);
}
If a secret is ever exposed, rotate it. The old secret is invalidated immediately:
curl -X POST "https://api.walkerstdata.com.au/v1/webhooks/subscriptions/{subscriptionId}/secret/rotate" \
-H "x-api-key: YOUR_API_KEY"
Delivery behaviour
- Success is any
2xxresponse from your endpoint. Return quickly (within 30 seconds) - do heavy processing asynchronously after acknowledging. - Failed deliveries (non-
2xx, timeout, or unreachable) are recorded but are not currently retried automatically. Treat webhooks as a fast-path notification and reconcile with a periodic job status poll as a safety net. - Idempotency: deliveries can arrive more than once. Deduplicate on
eventId, and make your handler idempotent.
Scoping to customers
Provide customerIds to scope a subscription to specific customers. Leave the list empty to create a catch-all subscription that receives events for every customer. You can add customers to an existing subscription later:
curl -X POST "https://api.walkerstdata.com.au/v1/webhooks/subscriptions/{subscriptionId}/customers" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "customerIds": ["524ba9bc-06e7-417e-bd54-b99785f5194a"] }'
To remove customers from a subscription, send DELETE to the same path with the customerIds to remove.
A subscription with an empty customer list is a catch-all. If you remove all customers from a scoped subscription, it reverts to receiving events for every customer - it does not go quiet.
Managing subscriptions
List your subscriptions (paginated - see Pagination):
curl -X GET "https://api.walkerstdata.com.au/v1/webhooks/subscriptions" \
-H "x-api-key: YOUR_API_KEY"
Two things to be aware of:
- Duplicate subscriptions are allowed. Registering the same event type and URL twice creates two independent subscriptions, and your endpoint will receive each event twice (with different signatures). If you're retrying a subscription create that may have succeeded, list your subscriptions first rather than blindly re-creating.
- Subscriptions can't currently be deleted via the API. If you need a subscription removed, contact us. Until it's removed, the subscription keeps delivering - have your endpoint acknowledge (
2xx) and discard events you no longer want.