Skip to content

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= (the next_cursor from 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. Answers 201 when it added one, 200 when 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.