Propvora

Developers

Build on your Propvora data

A REST API, signed webhooks and an MCP server, all scoped to one workspace and one API key. Available on the Agency and Enterprise plans.

Authentication

Create a key in Settings > Developers (workspace owners and admins, after re-confirming with an authenticator code or password). A key belongs to one workspace, has the scopes you choose, an optional expiry, and is shown once. Only a hash is stored. Revoke it at any time.

Request
curl https://propvora.com/api/v1/properties?limit=5 \
  -H "Authorization: Bearer pvk_YOUR_KEY"

Scopes

  • read:properties Read properties and units
  • read:tenancies Read tenancies
  • read:work Read work orders
  • write:work Create work orders
  • read:finance Read invoices and the finance summary
  • read:documents Read document metadata
  • webhooks:manage Manage webhook endpoints

A key can never see another workspace's data, and a request for a record in another workspace returns the same 404 as a record that does not exist.

Rate limits, pagination and errors

  • Each key is limited to 120 requests per minute. Over the limit you receive 429 with a Retry-After header.
  • Lists are newest first and cursor-paginated: ?limit=25 (1 to 100) and ?cursor= from the previous next_cursor. has_more tells you when to stop.
  • Amounts are integer minor units (pence or cents) with a currency code.
  • Write requests require an Idempotency-Key header. Repeating a request with the same key and body returns the original response; reusing a key with a different body returns 409.
Create a work order
curl -X POST https://propvora.com/api/v1/work-orders \
  -H "Authorization: Bearer pvk_YOUR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"property_id":"…","title":"Boiler service","description":"Annual service","priority":"normal"}'

Every error has the same shape. Quote request_id when contacting support.

Error
HTTP/1.1 403
{ "error": { "type": "permission_error", "code": "insufficient_scope",
             "message": "This API key does not have the \"write:work\" scope.", "request_id": "req_7f3a…" } }

API reference

Machine-readable: /api/v1/openapi.json (OpenAPI 3.1). Base URL: https://www.propvora.com/api/v1.

workspace

  • get/api/v1/me

    Describe the calling API key and its workspace

properties

  • get/api/v1/propertiesscope read:properties

    List properties

    Parameters:limitcursorstatuscitycountry_codeupdated_since

  • get/api/v1/properties/{id}scope read:properties

    Get a property

    Parameters:id

units

  • get/api/v1/unitsscope read:properties

    List units

    Parameters:limitcursorproperty_idstatus

  • get/api/v1/units/{id}scope read:properties

    Get a unit

    Parameters:id

tenancies

  • get/api/v1/tenanciesscope read:tenancies

    List tenancies

    Parameters:limitcursorproperty_idunit_idstatusupdated_since

  • get/api/v1/tenancies/{id}scope read:tenancies

    Get a tenancy

    Parameters:id

work orders

  • get/api/v1/work-ordersscope read:work

    List work orders

    Parameters:limitcursorproperty_idstatuspriorityupdated_since

  • post/api/v1/work-ordersscope write:work

    Create a work order Creates a work order in `draft` (default) or `open` status. Nothing is sent to suppliers or tenants by the API.

    Parameters:Idempotency-Key

  • get/api/v1/work-orders/{id}scope read:work

    Get a work order

    Parameters:id

documents

  • get/api/v1/documentsscope read:documents

    List document metadata

    Parameters:limitcursorproperty_idcategoryentity_typeentity_id

  • get/api/v1/documents/{id}scope read:documents

    Get document metadata

    Parameters:id

invoices

  • get/api/v1/invoicesscope read:finance

    List invoices

    Parameters:limitcursorstatusproperty_idtenancy_idissued_fromissued_to

  • get/api/v1/invoices/{id}scope read:finance

    Get an invoice

    Parameters:id

finance

  • get/api/v1/finance/summaryscope read:finance

    Invoiced, collected and outstanding totals per currency

    Parameters:fromto

webhook endpoints

  • get/api/v1/webhook-endpointsscope webhooks:manage

    List webhook endpoints

  • post/api/v1/webhook-endpointsscope webhooks:manage

    Create a webhook endpoint The signing secret is returned once, in this response only.

  • delete/api/v1/webhook-endpoints/{id}scope webhooks:manage

    Delete a webhook endpoint

    Parameters:id

Webhooks

Create endpoints in Settings > Developers (or with the webhooks:manage scope through the API). Each endpoint has a signing secret, shown once. Propvora sends a signed POST and expects a 2xx response within 10 seconds.

Delivery
POST /your/endpoint
Content-Type: application/json
Propvora-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Propvora-Event-Id: 6c1d0a5e-...      Propvora-Event-Type: work_order.status_changed
Propvora-Delivery-Id: 0b8f...         Propvora-Delivery-Attempt: 1

{
  "id": "6c1d0a5e-...",
  "type": "work_order.status_changed",
  "api_version": "2026-10-01",
  "created": 1790000000,
  "workspace_id": "a0000000-...",
  "data": { "id": "…", "reference": "WO-1042", "status": "completed", "previous_status": "in_progress" }
}

Events

  • tenancy.created tenancy created
  • tenancy.updated tenancy updated
  • work_order.created work order created
  • work_order.status_changed work order status changed
  • payment.succeeded payment succeeded
  • payment.refunded payment refunded
  • document.uploaded document uploaded
  • compliance.expiring compliance record expiring
  • booking.confirmed booking confirmed
  • booking.cancelled booking cancelled
  • quote.accepted quote accepted
  • ping test event from “Send test”

Verify the signature

The Propvora-Signature header is t=<unix seconds>,v1=<hex> where v1 is HMAC-SHA256 of {t}.{raw body} using your signing secret. Reject anything that does not match, and anything whose timestamp is more than 5 minutes old, so a captured request cannot be replayed. During a secret rotation the header can carry more than one v1.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// Use the RAW request body (a string/Buffer), not re-serialised JSON.
export function verifyPropvora(secret, rawBody, header, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;   // replay protection
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}
Python
import hmac, hashlib, time

def verify_propvora(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(","))
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance:      # replay protection
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Retries, dead-letter and auto-disable

  • Anything other than a 2xx (including timeouts and redirects, which are never followed) is retried after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours, 6 hours and 12 hours.
  • After the 8th failed attempt the delivery is dead-lettered. You can replay it from the delivery log.
  • After 10 consecutive failed attempts an endpoint is switched off automatically and workspace admins are notified. Re-enable it once your receiver is healthy.
  • Deliveries can arrive more than once and out of order. Use the event id to deduplicate.
  • In production, endpoints must be public https URLs on port 443 (or 8443). Private, loopback, link-local and cloud-metadata addresses are refused.

MCP server

Connect Claude or any MCP client that supports remote servers. The endpoint is https://www.propvora.com/api/mcp (Streamable HTTP, stateless) and authenticates with the same workspace API key. OAuth sign-in for connectors is planned; until then use a key with only the scopes the assistant needs.

Claude Code
claude mcp add --transport http propvora https://propvora.com/api/mcp \
  --header "Authorization: Bearer pvk_YOUR_KEY"
Other clients
{
  "mcpServers": {
    "propvora": { "type": "http", "url": "https://propvora.com/api/mcp",
                  "headers": { "Authorization": "Bearer pvk_YOUR_KEY" } }
  }
}

Tools

  • get_workspace, list_properties, get_property, list_units
  • list_tenancies, get_tenancy, list_work_orders, get_work_order
  • list_documents (metadata only), list_invoices, get_finance_summary
  • create_work_order needs write:work. It does not create anything: it files an approval request, and a workspace member approves it in Agent Center.

Tools listed to a client depend on the key's scopes. Reads are free. Record text is written by other people, so results are labelled as untrusted data and cleaned of hidden control characters, but you should still review what your assistant does with it. Calls are rate-limited and audited.