Webhooks
Signed JSON events sent to your own endpoint when the data you watch changes or a job finishes.
Webhooks come with Market Analytics. You can read the documentation below, but registering endpoints needs that plan.
See pricingStore it wherever your receiver verifies X-Soro-Signature. We will not show it again; to rotate it, delete the endpoint and register it again.
| When | Event | Delivery ID | Attempt | Result | ms |
|---|
Every delivery is a POST of a JSON envelope, with headers that identify and sign it:
X-Soro-Event: rate.changed X-Soro-Delivery-Id: dlv_9f2c1e8a7b3d4c05 X-Soro-Signature: t=1792000000,v1=<hex hmac-sha256(secret, t + "." + rawBody)> X-Soro-Attempt: 1 Content-Type: application/json
To verify a delivery, recompute the HMAC over the timestamp, a dot and the raw request body (the bytes exactly as received, before any JSON parsing), and compare the result in constant time. Reject any t older than five minutes to prevent replays.
// Node.js receiver
const crypto = require("crypto");
function verify(secret, header, rawBody) {
const m = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header || "");
if (!m) return false;
if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${m[1]}.${rawBody}`).digest("hex");
return expected.length === m[2].length && crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(m[2], "hex"));
}
The envelope:
{
"id": "dlv_9f2c1e8a7b3d4c05",
"object": "event",
"type": "rate.changed",
"created": 1792000000,
"api_version": "2026-09-11",
"data": { "build": 12, "built_at": "2026-09-11T02:00:00Z", "count": 3, "truncated": 0, "items": [ … ] }
}
| Event | When | Each item in data.items |
|---|---|---|
| rate.changed | A watched rate moved past your threshold or was removed (change: "removed"). The scope is a hospital, payer, provider or contract. | scope, hospital | payer | npi + payer_id | workspace, slug, state, key ("CPT:27447"), previous, current, amount, change_pct, build, watch{kind,target} |
| source.discovered | A new hospital price file appeared in a watched state (change: "new"), or a watched hospital's file was replaced (change: "replaced"). | change, slug, hospital, state, format_changed, build |
| source.unavailable | A watched hospital's file stopped answering (4xx, 5xx or no status) since the previous run. | slug, hospital, state, host, status (broken | error | unreachable), http_status, build |
| payer.coverage_added | A watched payer priced codes it had not priced before. | payer, codes_added, build |
| dataset.refreshed | A rate history build finished. This is sent to every account with at least one watch. | build, built_at, previous_build |
| market.movement | The reimbursement index moved by 5% or more, nationally or in a watched state or service line. | scope, change_pct, build |
| yield.analysis_ready | A Contract Yield workspace finished analysing an upload, so its opportunities, denials and queues are current. | workspace, kind, rows, built_at |
| job.completed | An async export or batch job finished (done or failed). This event is sent as soon as the job finishes, not with the batch of build events, and the envelope's data is the job itself. | id, kind, status, createdAt, finishedAt, rows, files[{name,bytes,sha256}], error, poll |
Dollar fields (previous, current, amount) are included only while your paid plan is active; percentages and directions are always included. Each delivery times out after 10 seconds and gets up to three attempts with backoff (0 s, 2 s, 8 s). An endpoint is disabled after 20 consecutive failures; you can re-enable it here. Any 2xx response counts as success, and redirects are not followed. We keep the last 50 deliveries per endpoint. The same registry is available through the API: GET/POST /v1/webhooks, GET /v1/webhooks/{id}/deliveries, POST /v1/webhooks/{id}/test.