# ucc-api (ucc.cap-fi.com) Headless CRM + control plane for the UCC lead pipeline (Efi's ucc-sorting repo on this box). Two things live here: Efi's lead database (read-only through this API; tables leads, people, filings, lead_filings, pulls, lead_history, ready_queue, finished, lead_pointers; lead_id is the stable key) and our own database (runs, events, email_verdicts, datasets, efforts). Full OpenAPI at /openapi.json and /docs. Human-readable reference: API.md in the repo. ## Auth Every call except GET /health, GET /llms.txt and GET / needs a key: X-API-Key: (or Authorization: Bearer ). /hooks/* also accepts ?key=. The key names the actor (mendel / efi), stored on every write. Errors are always {"error": "", "hint": ""}. ## Pagination Every list returns {"items": [...], "total": n, "limit": l, "offset": o}. Query params limit (default 100, max 1000) and offset. Lists are newest first. ## Endpoints GET /health service state, lead_db exists|missing|busy, leads count, last pull, running run GET /schema every table + column + row count of both DBs. READ THIS BEFORE WRITING SQL FOR /query GET /metrics counts: leads by pull, runs, events 7d/30d, verdicts, efforts totals GET /leads?q=&pull_id=&state= search (q = business / person name, case-insensitive, or an exact lead_id) GET /leads/{lead_id} lead + people + filings + history + emails[] + our events + verdicts (404 -> search with /leads?q=) GET /filings?lead_id=&since= Efi's filings (since = YYYY-MM-DD) GET /pulls {"pulls": Efi's pulls table, "pull_log": rawlane/pull_log.csv lines} GET /ready ready_.csv and lookup_.csv sheets with size, mtime, rows GET /ready/{name} the CSV itself; GET /ready/{name}/rows?limit=&offset= as JSON GET /scorecard, GET /issues blind-check results (ucc_organize/scorecard.csv, issues_log.csv) POST /runs/pull {"from": "YYYY-MM-DD", "to": "YYYY-MM-DD", "resort": false} all optional -> 202 run row POST /runs/lookup {"pull_id": "2741497", "stage": "plan"} free {"pull_id": "2741497", "stage": "run", "confirm": true} SPENDS MONEY (~$0.017/lead); 400 without confirm POST /runs/baseline one-time first build of the lead database (python3 rawlane/pull.py --baseline) GET /runs?status= runs; GET /runs/{id}; GET /runs/{id}/log?tail=200 (plain text); runs carry cost_usd for helper runs GET /pullucc/next THE RECIPE'S NEXT STEP: {"step": pull|lender-research|await-yes-on-verdicts|add-verdicts|check|save|done|wait, "why", "call", "next_call": {method, path, body}, "state"}. Call it, do what it says, call it again. POST /runs/lender-research {"confirm": true, "model": "sonnet"} step 4: Claude helpers research rawlane/out/new_lenders.csv names (research_todo=Y) into research/lender_gap//verdicts_partN.csv + the spot check. SPENDS ANTHROPIC CREDIT. GET /lender-research days with verdict counts; GET /lender-research/{date} the verdict rows, our-money first, with on_list POST /events {"kind": "verdicts.approved", "data": {"date": ""}} log Efi's yes on the our-money list (step 5's gate) POST /runs/add-verdicts {"date": "YYYY-MM-DD", "confirm": true} step 5: pull.py --add-verdicts (re-sorts) POST /runs/check {"confirm": true, "model": "sonnet"} step 6: two reviewer helpers + judge on the open check folder, then check.py finish (scorecard line). SPENDS ANTHROPIC CREDIT. POST /runs/save step 7: git add/commit of the logbook, lender list, scorecard, issues log and today's lender_gap folder; pull --rebase; push POST /events {"kind": "pushed", "lead_id": "L123", "email": "a@b.com", "campaign": "...", "data": {...}, "note": "..."} or a JSON list of those. Header Idempotency-Key: makes a repeat within 24 h return the first response. GET /events?kind=&lead_id=&email=&campaign=&since= kind ending in "." is a prefix (kind=instantly.) POST /verify {"emails": ["a@b.com", ...] (max 200), "lead_id": "L123", "force": false} reuses a verdict under 30 days old; otherwise asks verify.cap-fi.com (4 in parallel). Idempotency-Key works here too. GET /verdicts?email=&lead_id=&status=&since= POST /datasets {"name", "source", "description", "rows", "path", "status", "notes"}; GET /datasets; GET /datasets/{id} (with efforts); PATCH /datasets/{id} POST /efforts {"dataset_id", "ts", "channel", "campaign", "leads_sent", "bounces", "replies", "interested", "notes"}; GET /efforts?dataset_id=&campaign=&since=; PATCH /efforts/{id} POST /query {"sql": "SELECT ...", "params": [], "limit": 500, "explain": false} read-only; our tables are main., Efi's are leads.
; one SELECT/WITH, no ; inside, 10 s cap, max 5000 rows. explain: true compiles and returns the column names without running. Response has elapsed_ms. POST /hooks/instantly Instantly webhook JSON -> event kind instantly. (email, campaign, full payload in data) ## Event kinds (free-form; use these when they fit) pushed, bounce, reply, interested, not_interested, note, enriched, called, instantly., pull.finished (written by the service after a pull) ## Run rules - One run at a time (pull, lookup or baseline): a second POST /runs/* gets 409 until it finishes. GET /health running_run tells you. - A pull takes minutes; poll GET /runs/{id} until status is done or failed, then read /runs/{id}/log. After a successful pull an event kind=pull.finished holds the new pull_log line. - lookup stage=plan is free and prints the cost estimate; stage=run spends money and is refused (400) without "confirm": true. Never send confirm: true without Efi's explicit yes. - While a pull is running Efi's database may be locked: lead endpoints answer 503 "lead database busy"; try again later. - POST /runs/baseline only on a fresh box with no lead database; pull.py itself refuses it otherwise. - The recipe after a pull (GET /pullucc/next walks it): lender-research (Claude helpers, credit) -> show Efi the our-money list and log his yes as an event verdicts.approved -> add-verdicts -> check (Claude helpers, credit) -> save. Never log verdicts.approved or send add-verdicts without Efi's explicit yes; never run lender-research or check without confirm: true. ## Example (curl) curl -s -H "X-API-Key: $UCC_API_KEY" "https://ucc.cap-fi.com/leads?q=acme&limit=5" curl -s -H "X-API-Key: $UCC_API_KEY" -H "Content-Type: application/json" -d '{"kind":"pushed","lead_id":"L123","campaign":"BC Aged UCC 10/26"}' https://ucc.cap-fi.com/events curl -s -H "X-API-Key: $UCC_API_KEY" -H "Content-Type: application/json" -d '{"sql":"SELECT l.lead_id, l.legal_name, e.kind FROM leads.leads l JOIN main.events e ON e.lead_id = l.lead_id LIMIT 10"}' https://ucc.cap-fi.com/query