Webhooks

Signed JSON events sent to your own endpoint when the data you watch changes or a job finishes.

Add an endpoint
Events
Endpoints
How deliveries work

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": [ … ] }
}
EventWhenEach item in data.items
rate.changedA 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.discoveredA 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.unavailableA 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_addedA watched payer priced codes it had not priced before.payer, codes_added, build
dataset.refreshedA rate history build finished. This is sent to every account with at least one watch.build, built_at, previous_build
market.movementThe reimbursement index moved by 5% or more, nationally or in a watched state or service line.scope, change_pct, build
yield.analysis_readyA Contract Yield workspace finished analysing an upload, so its opportunities, denials and queues are current.workspace, kind, rows, built_at
job.completedAn 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.