API and webhooks
Last updated 10-15-2026
Connect Vurno to your own systems: read your clients and agents, add clients from your loan origination system or CRM, and get a signed webhook the moment a client gets hot, lists their home or hits a rate target. The API and webhooks are part of the Team plan; owners and admins set them up under Settings → Developers.
Authentication
Create a key under Settings → Developers and send it on every request. Keys are shown once; we keep only a one-way hash. A key has the read scope, and optionally write to add clients. Revoke a key there and it stops working at once.
curl https://app.vurno.io/api/v1/workspace \
-H "Authorization: Bearer vk_…"Each key may make 120 requests a minute; past that you get 429 with a Retry-After header. Errors always look like { "error": { "code": "…", "message": "…" } }.
Endpoints
GET /api/v1/workspace: the workspace the key belongs to, and the key's scopes.GET /api/v1/clients: your clients, newest first.?limit=(1 to 100, default 50),?cursor=(thenext_cursorfrom the last page),?updated_since=(an ISO date).GET /api/v1/clients/:id: one client.POST /api/v1/clients(write): add a client, or update the one with the same email, external id or address. Same rules as the importer: your plan's allowances, duplicate matching and validation. Answers201when it added one,200when it updated one.GET /api/v1/agents: your partner agents, same paging.
A client
{
"id": "cmv8x2k1p0003…",
"name": "Dana Whitfield",
"firstName": "Dana", "lastName": "Whitfield",
"email": "dana@example.com", "phone": "+19045550142",
"address": { "line1": "88 Tarpon Bay Ct", "city": "Orange Park", "state": "FL", "zip": "32073" },
"estimatedValue": 412300, "loanBalance": 268900, "equity": 143400, "ltv": 0.652,
"loanRate": 6.875, "targetRate": 6.0, "targetReached": false,
"heatScore": 71, "isHot": true,
"listingStatus": null, "onMarket": false,
"homePageUrl": "https://app.vurno.io/h/…"
}Adding one
curl -X POST https://app.vurno.io/api/v1/clients \
-H "Authorization: Bearer vk_…" -H "Content-Type: application/json" \
-d '{ "firstName": "Dana", "lastName": "Whitfield", "email": "dana@example.com",
"address": "88 Tarpon Bay Ct", "city": "Orange Park", "state": "FL", "postalCode": "32073",
"loanAmount": 285000, "interestRate": 6.875, "closeDate": "2024-03-15",
"loanOfficer": "rhea@harborlending.com" }'Fields: firstName, lastName, fullName, email, phone, externalId, address, city, state, postalCode, loanAmount, interestRate, closeDate, loanType, currentBalance, termMonths, loanOfficer (an email or name of someone in the workspace). A client needs a name (fullName, or firstName and lastName), an address and a postalCode; sending one that matches an existing client by email, external id or address updates it.
Webhooks
Add an endpoint under Settings → Developers, pick the events it should get (or all of them), and copy its signing secret. Vurno sends a POST with a JSON body for each event, and these headers:
X-Vurno-Event: the event type.X-Vurno-Delivery: an id that stays the same across retries. Use it to ignore a delivery you already handled.X-Vurno-Attempt: 1 for the first try.X-Vurno-Signature:t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body">under your endpoint's secret.
Checking the signature
import { createHmac, timingSafeEqual } from "node:crypto";
// header = req.headers["x-vurno-signature"], body = the raw request body (string)
function fromVurno(secret, body, header, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
return parts.v1?.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}Compute it over the raw body, before any JSON parsing, and refuse anything older than a few minutes so a captured request can't be replayed.
Retries
Answer with any 2xx within 8 seconds. If your server times out, can't be reached, or answers 408, 425, 429 or 5xx, Vurno tries again after 1 minute, 5 minutes, 30 minutes, 2 hours and 8 hours. Any other 4xx is treated as final. An endpoint that fails 50 deliveries in a row is switched off; turn it back on in Settings. The last deliveries and their answers are listed there too.
Events
client.hot: A client's heat score crosses the hot line.client.listed: A client's home is listed, pending or coming soon.client.sold: A client's home sells.client.equity_changed: A client's estimated value moves 3% or $10k.client.rate_cross: Today's rate is far enough under a client's rate to save $250/mo.client.engaged: A client opens their home page or clicks an email.client.rate_target_hit: Rates reach the target a client asked to be told about.lead.home_report: Someone asks for their home report through your website widget.lead.rate_watch: Someone sets a rate target through your website widget.agent.listed: A watched agent lists a home.agent.open_house: A watched agent holds an open house.agent.sold: A watched agent closes a sale.test: sent when you press Send test.
Every event carries type, occurredAt, the workspace, a one-line summary, and the client (the shape above) or agent it's about.
Versioning and help
The API is versioned in the path. Within v1 we only add: new fields, endpoints and event types, never a rename or removal, so ignore what you don't recognize. Questions, or an endpoint you need: support@vurno.io.