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
GET/v1/clients
GET/v1/transactions?status=UNDER_CONTRACT
status filter: LISTING, UNDER_CONTRACT, PENDING, CLOSED, CANCELLED.POST/v1/transactions
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
POST/v1/contacts
name required; email, phone, category optional.GET/v1/tasks?transactionId=…
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
- List endpoints cap at 200 to 500 records and don't paginate yet; pagination is coming.
- There are no rate limits yet. Please be reasonable, and don't build a poller that hammers every second.
- Webhook deliveries are at-least-once: dedupe on the event payload if it matters.
- The API grows on request like everything else here: ask at hello@freeholdtc.dev or on GitHub.