API reference

Read and write everything in your workspace: transactions, contacts, and tasks, plus signed webhooks when things happen. The API is the same whether you're on Freehold Cloud or self-hosting.

Authentication

Create an API key in Settings → API keys (workspace admins only). Keys look like fh_live_…, are shown once, and give full read/write access to that one workspace. Send the key as a bearer token. Only a hash of the key is stored, and you can revoke any key instantly.

curl https://your-freehold-host/v1/transactions \
  -H "Authorization: Bearer fh_live_your_key_here"

On Freehold Cloud the base URL is https://freeholdtc.dev/api/v1. Self-hosting has the same routes under /api/v1, plus a standalone API service on port 3001 (paths start at /v1).

Endpoints

GET/v1/account

Workspace overview: name, plan, and counts of active/closed transactions, contacts, clients, and open tasks.

GET/v1/clients

Your clients with transaction counts and portal links (audience, active, last opened).

GET/v1/transactions?status=UNDER_CONTRACT

List transactions, newest first (up to 200). Optional status filter: LISTING, UNDER_CONTRACT, PENDING, CLOSED, CANCELLED.

POST/v1/transactions

Create a transaction. propertyAddress is required; status, side, city, state, zip, purchasePrice (dollars), contractDate and closeDate (YYYY-MM-DD) are optional. Returns 201 with the created record and fires the transaction.created webhook.

GET/v1/contacts

List contacts alphabetically (up to 500).

POST/v1/contacts

Create a contact. name required; email, phone, category optional.

GET/v1/tasks?transactionId=…

List tasks by due date, optionally scoped to one transaction.
curl -X POST https://your-freehold-host/v1/transactions \
  -H "Authorization: Bearer fh_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "propertyAddress": "825 Birchwood Lane",
    "city": "Naperville",
    "state": "IL",
    "status": "UNDER_CONTRACT",
    "side": "BUY_SIDE",
    "purchasePrice": 462000,
    "closeDate": "2026-08-21"
  }'

Webhooks

Add endpoints in Settings → Webhooks. Freehold POSTs JSON for the events you choose: transaction.created and task.completed today, with more coming. Failed deliveries retry up to 3 times with backoff; treat webhooks as hints and reconcile with the REST API for anything critical.

Every delivery is signed. The freehold-signature header carries t=<unix>,v1=<hex> where v1 is HMAC-SHA256 of t + "." + body using your endpoint's secret. Reject anything older than 5 minutes.

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

function verify(secret, header, body) {
  const { t, v1 } = Object.fromEntries(
    header.split(",").map((p) => p.split("=")),
  );
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${t}.${body}`)
    .digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(v1, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Use it from Claude

The repo ships a ready-made Claude skill (skills/freehold/): drop it into ~/.claude/skills/, set FREEHOLD_API_KEY and FREEHOLD_API_URL, and ask Claude things like "what closes this week?" or "has my client opened their portal?" — answered live from your workspace.

Honest notes