# Greenfinch: integration guide for coding agents This document is generated from the Greenfinch developer documentation. Part 1 is what a first integration needs. Part 2 is the full reference: every endpoint, tool, refusal code and delivered field. The short version is at https://www.greenfinch.ai/developers/llms-essentials.txt. The whole document, with the full reference, is at https://www.greenfinch.ai/developers/llms.txt. Both are explained at https://www.greenfinch.ai/developers/coding-agents, and the full documentation starts at https://www.greenfinch.ai/developers. # Part 1: Essentials ## What Greenfinch is Greenfinch finds commercial properties and the people who own, manage and make decisions about them, for companies that sell services to those buildings. Your code can search those properties, firms and people, unlock their details and move leads through your pipeline. It can use the REST API or the MCP connector. ## Base URLs - REST API: `https://app.greenfinch.ai/api/v1` - MCP connector: `https://app.greenfinch.ai/api/v1/mcp` (JSON-RPC 2.0 over HTTPS) - All requests use HTTPS. Request bodies are JSON with `Content-Type: application/json`. ## Authentication and plans Send an organization API key as a bearer token on every request, REST and MCP alike: ```http Authorization: Bearer gfk_… ``` - An organization admin creates keys in the Greenfinch app under Org admin → Integrations. A key starts with `gfk_` and is shown once. Read it from an environment variable such as `GREENFINCH_API_KEY`; never put it in source code. - `GET /me` answers for any live key, whatever its scopes. Call it first to check the key: ```bash curl "https://app.greenfinch.ai/api/v1/me" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` - A key can call only what its scopes allow; a call without the scope is refused with `403` `SCOPE_NOT_GRANTED`. The calls below need these Enterprise scopes, and an admin must tick each one deliberately: - `mcp`: Let an autonomous AI agent use every Greenfinch connector tool for your org — search, reveal, qualify, and write back — subject to your agent settings and daily limits. - `data:read`: Search and look up properties, contacts, and organizations — the same results your team sees in the app. Looking at a contact your plan's coverage includes (but you haven't unlocked) counts against your organization's daily contact limit. - `data:unlock`: Reveal a contact's details or unlock a property, spending credits exactly as it would in the app. Anything already unlocked is not charged again, and your daily limits apply. - The REST search, read and unlock endpoints require the Enterprise plan and the `data:read` scope (`data:unlock` for unlocking and revealing). The plan is checked live on every call, never stored on the key. Any other plan gets a refusal, not a smaller set of endpoints: ```json // HTTP 403 { "success": false, "error": "API Access isn't included on your current plan. Contact sales for API access.", "code": "UPGRADE_REQUIRED", "feature": "apiAccess", "minimumTier": "enterprise" } ``` ## Looking and taking Looking is covered by the subscription. Taking a record draws on the allowance. Looking and taking are bounded differently: - A person reading a property, a firm or a person on screen in the Greenfinch app is doing what the plan already paid for, and is not rationed. - A record delivered to a program (over REST, through the connector, or to a webhook) counts as taken. Taken records are limited by the request rate, the daily data limits and, on Enterprise, the records allowance: how many distinct records may be taken in a billing period. - A search result is a summary (an identifier, an address, a one-line description, a count), so it never counts as taken and never draws the records allowance. Searching is limited only by the request rate. - Credits pay for seeing a record the organization is not already licensed for, and for research. Unlocking a property or revealing a contact spends credits; anything already unlocked is not charged again. ## First calls Every response uses one envelope. Success: ```json { "success": true, "data": { "…": "endpoint-specific" } } ``` Error: ```json { "success": false, "error": "Human-readable message", "meta": { "code": "MACHINE_CODE" } } ``` - Check the HTTP status first, then `success`. `data` is present only on success. - A `401`, and a `400` for bad arguments, carry no code: branch on the status. - Branch on the machine code, never on the message. Read `meta?.code ?? code`: errors raised before the endpoint runs carry the code at the top level. - On the search, read and unlock endpoints, `meta.issues` lists each problem as `{ field, message }`, and an unknown body key or query parameter is a `400`, not ignored. - Responses that carry data are sent with `Cache-Control: no-store`. Do not cache them. - Each search, read and unlock endpoint below names its MCP twin. It runs the same code as that tool and returns what the tool returns as `data`, with the same territory rules, charges and daily limits. ### POST /properties/search Searches commercial properties in one of three ways: free text (property name, address, city, ZIP code, the owner named on the county record, or the researched owner on properties you have unlocked or your plan covers), a location filter (county FIPS, ZIP, city or county name) plus any attribute filters, or a map bounding box. Use county or location mode to get a true match count and a category breakdown; use free text to find a specific property. Scope: `data:read`. MCP twin: `greenfinch_search_properties`. Cost: Free. Request body (JSON): - `query` (string): Free-text search across property name, address, city, ZIP code and owner: the owner named on the county record, and the researched owner on properties your organization has unlocked or your plan covers. The text is matched as typed: % and _ are ordinary characters, not wildcards. - `bounds` (object) - `bounds.minLat` (number, required) - `bounds.maxLat` (number, required) - `bounds.minLon` (number, required) - `bounds.maxLon` (number, required) - `filters` (object) - `filters.categories` (string[]) - `filters.subcategories` (string[]) - `filters.countyFips` (string[]): 5-digit county FIPS codes to search, e.g. ["51760"] for Richmond City, VA. Exact county scoping — use this instead of `bounds` whenever the question is about a county. - `filters.zipCodes` (string[]) - `filters.cities` (string[]) - `filters.counties` (string[]): State-qualified county slugs (e.g. "middlesex-nj") or legacy bare names. OR-combined with zipCodes/cities, then ANDed with every other filter — matches the app's location panel. - `filters.buildingClasses` (string[]) - `filters.acTypes` (string[]) - `filters.heatingTypes` (string[]) - `filters.roofTypes` (string[]) - `filters.exteriorWallTypes` (string[]) - `filters.poolTypes` (string[]) - `filters.fenceTypes` (string[]) - `filters.garageTypes` (string[]) - `filters.minLotSqft` (number) - `filters.maxLotSqft` (number) - `filters.minBuildingSqft` (number) - `filters.maxBuildingSqft` (number) - `filters.minUnits` (integer): Displayed unit count — a reviewed correction when one is in force (which may be no count), else the researched value when known, else the assessor's. - `filters.maxUnits` (integer) - `filters.minPropertyValue` (number) - `filters.maxPropertyValue` (number) - `filters.minYearBuilt` (number) - `filters.maxYearBuilt` (number) - `filters.minFloors` (integer) - `filters.maxFloors` (integer) - `filters.minBedrooms` (integer) - `filters.maxBedrooms` (integer) - `filters.minBathrooms` (number) - `filters.maxBathrooms` (number) - `filters.enrichmentStatus` ("researched" | "not_researched") - `filters.saleRecencyBuckets` (string (one of 5)[]): e.g. ["3_months", "over_a_year"] — sold or transferred within the last N (the newer of the Last Sold and Last Transfer dates). One of: 1_month, 3_months, 6_months, 12_months, over_a_year. - `filters.organizationId` (string): Properties linked to this organization. UUID. - `filters.contactId` (string): Properties linked to this contact. UUID. - `filters.changedKinds` ("ownership_transfer" | "sale" | "manager_change"[]): Only properties with a recorded change of one of these kinds: ownership_transfer (the county records a new owner, backed by a newer recorded transfer), sale (a newer sale date was recorded), manager_change (a later research pass found a different managing firm, judged a real change). Combine with changedWithinDays; call greenfinch_get_changes for the evidence. Available once change filters (signals) are switched on for Greenfinch; until then a call that passes this is refused with SIGNALS_NOT_ENABLED, never run without it. - `filters.changedWithinDays` (integer): Only changes the product detected within this many days (1 to 3650). Alone, it matches any of the kinds above. Available once change filters (signals) are switched on for Greenfinch; until then a call that passes this is refused with SIGNALS_NOT_ENABLED, never run without it. - `filters.leaseTypes` ("triple_net" | "gross" | "modified_gross" | "ground_lease"[]): Only properties whose researched lease arrangement is one of these (triple_net = Triple-net, gross = Gross, modified_gross = Modified gross, ground_lease = Ground lease). Owner-occupied and sale-leaseback are not filterable: they describe who owns the property and are shown on its detail once its research is unlocked. Research records a lease type only when a source it read states it (a leasing flyer, listing or article), so most properties have none and never match; the property's detail carries the source. Available once the lease-type field is switched on for Greenfinch; until then a call that passes this is refused with LEASE_TYPE_NOT_ENABLED, never run without it. - `limit` (integer): Max results, default 50, max 200. - `cursor` (string): Pagination cursor from a prior `query`-mode call. - `offset` (integer): Rows to skip in a county/bounds search — page 2 of a ranked list is offset=limit. (min 0, max 10000) - `sortBy` (string (one of 19)): Every sort the app's property search offers — works in query mode AND county/bounds mode. Defaults to "relevance". Numeric and date sorts place properties with no value LAST in both directions; the text sorts (commonName, ownerName, address, city, owner) treat a missing value as the highest value, so it sorts last ascending and FIRST descending. "owner" sorts the owner of record as the county states it; "ownerName" sorts the assessor's owner name, a separate value. "address" sorts the county's address line, not the tidied address shown in results. One of: relevance, commonName, lotSqft, buildingSqft, totalUnits, contactCount, landscapableSqft, roofAreaSqft, buildingFootprintSqft, yearBuilt, numFloors, ownerName, propertyValue, updatedAt, lastEnrichedAt, lastSaleDate, address, city, owner. - `sortOrder` ("asc" | "desc") - `verbosity` ("compact" | "full"): compact (default): the decision fields — id, address and location, owner, category, sizes, value, year, a short description. full: adds the county account numbers of a multi-parcel property, grouping flags and the assessor's owner-name variants. Compact rows are about 40% smaller; prefer compact unless you need those. Worked call: ```bash curl -X POST "https://app.greenfinch.ai/api/v1/properties/search" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filters": { "countyFips": [ "48085" ], "categories": [ "Office" ], "minBuildingSqft": 20000 }, "sortBy": "buildingSqft", "sortOrder": "desc", "limit": 2 }' ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "results": [ { "id": "3f2a9c1e-0000-4000-8000-000000000001", "address": "1200 Commerce Pkwy", "city": "Plano", "state": "TX", "zip": "75074", "county": "Collin", "lat": 33.0198, "lon": -96.6989, "owner": "Northgate Business Park LLC", "commonName": "Northgate Business Park", "assetCategory": "Office", "assetSubcategory": "Office Building", "totalParval": 18450000, "yearBuilt": 2004, "lotSqft": 217800, "buildingSqft": 96000, "numFloors": 4, "aiRationale": "Four-story multi-tenant office building on a landscaped campus with surface parking...", "enrichmentStatus": "completed" } ], "total_matching": 214, "has_more": true, "returned": 1, "offset": 0, "next_offset": 1, "next_cursor": null, "category_breakdown": [ { "category": "Office", "count": 214, "withValue": 209, "totalValue": 1934500000 } ], "sorted_by": { "field": "buildingSqft", "direction": "desc" }, "searched_scope": { "scoped": true, "countyCount": 2, "states": ["TX"] }, "truncation_note": "Showing 1 of 214 matches. Ranked by buildingSqft (desc); pass offset=1 for the next page. Use total_matching, never the row count, when reporting how many exist." } } ``` ### GET /properties/{id}/detail Fetches one property's full detail: address, size, age, category, assessed value, ownership and management (when unlocked), the full AI write-up, and its contacts. Read researchState first: it tells you whether to start research, unlock the property, or read the contacts directly. Scope: `data:read`. MCP twin: `greenfinch_get_property`. Cost: No credits. Contacts your organization can see only through its geographic subscription (not through a purchase) count against the daily contact-disclosure limit; if that limit is used up they come back masked instead. Request: no body. `{id}` is the Greenfinch UUID of the property. Worked call: ```bash curl "https://app.greenfinch.ai/api/v1/properties/3f2a9c1e-0000-4000-8000-000000000001/detail" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "property": { "id": "3f2a9c1e-0000-4000-8000-000000000001", "address": "1200 Commerce Pkwy", "city": "Plano", "state": "TX", "zip": "75074", "county": "Collin", "lat": 33.0198, "lon": -96.6989, "lotSqft": 217800, "buildingSqft": 96000, "yearBuilt": 2004, "yearBuiltDetail": { "basis": "per_building", "oldest": 1998, "newest": 2004, "primaryBuildingBasis": "area", "buildings": 3, "buildingsTotal": 3 }, "numFloors": 4, "numFloorsDetail": { "basis": "per_building", "max": 4, "areaWeightedMean": 3.51, "primaryBuilding": 4, "primaryBuildingBasis": "area", "buildings": 3, "buildingsTotal": 3 }, "effectiveYearBuilt": 2015, "effectiveYearBuiltDetail": { "basis": "per_building", "oldest": 2009, "newest": 2015, "primaryBuildingBasis": "area", "buildings": 2, "buildingsTotal": 3 }, "heatingType": "Rooftop Package Unit", "heatingTypeDetail": { "basis": "per_building", "buildings": 3, "buildingsTotal": 3, "areaShare": 1, "values": [ { "value": "Rooftop Package Unit", "buildings": 2, "areaShare": 0.844 }, { "value": "Heat Pump", "buildings": 1, "areaShare": 0.156 } ] }, "coolingType": "Rooftop Package Unit", "coolingTypeDetail": { "basis": "per_building", "buildings": 3, "buildingsTotal": 3, "areaShare": 1, "values": [ { "value": "Rooftop Package Unit", "buildings": 2, "areaShare": 0.844 }, { "value": "Heat Pump", "buildings": 1, "areaShare": 0.156 } ] }, "primaryConstructionType": "Concrete", "primaryConstructionTypeDetail": { "basis": "per_building", "buildings": 3, "buildingsTotal": 3, "areaShare": 1, "values": [{ "value": "Concrete", "buildings": 3, "areaShare": 1 }] }, "exteriorWallMaterial": null, "exteriorWallMaterialDetail": null, "foundationType": "Slab on Grade", "foundationTypeDetail": { "basis": "per_building", "buildings": 3, "buildingsTotal": 3, "areaShare": 1, "values": [{ "value": "Slab on Grade", "buildings": 3, "areaShare": 1 }] }, "roofCover": null, "roofCoverDetail": null, "roofShape": null, "roofShapeDetail": null, "qualityGrade": "Good", "qualityGradeDetail": { "basis": "per_building", "buildings": 3, "buildingsTotal": 3, "areaShare": 1, "values": [ { "value": "Good", "buildings": 2, "areaShare": 0.844 }, { "value": "Average", "buildings": 1, "areaShare": 0.156 } ] }, "hasPool": false, "hasPoolDetail": { "basis": "per_building", "count": 0, "buildings": 3, "buildingsTotal": 3 }, "hasSprinklers": true, "hasSprinklersDetail": { "basis": "per_building", "count": 2, "buildings": 2, "buildingsTotal": 3 }, "buildingAttributesBasis": "per_building", "footprintArea": 21500, "footprintAreaSource": "measured", "assetCategory": "Office", "assetSubcategory": "Office Building", "commonName": "Northgate Business Park", "nameLocked": false, "beneficialOwner": "Northgate Holdings Group", "beneficialOwnerType": "private_investor", "managementCompany": "Lakeside Property Services", "aiRationale": "Four-story multi-tenant office building on a landscaped campus. Managed by Lakeside Property Services on behalf of Northgate Holdings Group...", "enrichmentStatus": "completed", "lastEnrichedAt": "2026-08-20T14:11:00.000Z", "researchedAt": "2026-08-20T14:11:00.000Z", "researchTier": { "displayName": "Base research model", "description": "Our standard AI research pass — fast and accurate for the large majority of properties.", "isUpgradeAvailable": true }, "totalParval": 18450000, "premiumDataRedacted": false }, "contacts": [ { "id": "3f2a9c1e-0000-4000-8000-000000000201", "fullName": "Dana Whitfield", "normalizedName": "dana whitfield", "contactType": "individual", "email": "dana.whitfield@example.com", "normalizedEmail": "dana.whitfield@example.com", "emailStatus": "valid", "emailValidationStatus": "valid", "alternateEmails": [], "phone": "(214) 555-0142", "normalizedPhone": "+12145550142", "phoneLabel": "Direct", "phoneExtension": null, "title": "Facilities Manager", "companyDomain": "example.com", "employerName": "Lakeside Property Services", "roleChangedAt": null, "employerChangedAt": null, "likelyRoleChangeAt": null, "likelyEmployerChangeAt": null, "currentRoleStartedAt": "2022-03-01T00:00:00.000Z", "currentEmployerStartedAt": "2019-06-01T00:00:00.000Z", "linkedinUrl": "https://www.linkedin.com/in/example-dana-whitfield", "linkedinStatus": "verified", "location": "Dallas, TX", "enrichedAt": "2026-08-20T14:11:00.000Z", "createdAt": "2026-08-20T14:11:00.000Z", "role": "property_manager", "relationshipStatus": "active", "revealed": true, "…": "…" } ], "contactsSummary": null, "researchRevealed": true, "researchState": "revealed", "revealSource": "paid", "coveredByPlan": true } } ``` ### POST /properties/{id}/unlock Unlocks a researched property for your organization, which gives you its full research and every contact attached to it. Use it before reading a property's contacts or fetching its lead bundle. Scope: `data:unlock`. MCP twin: `greenfinch_reveal_property`. Cost: 10 credits to unlock the property, unless your organization already unlocked it or your plan covers its county. Retrying is safe: a property that is already unlocked costs 0. Request: no body. `{id}` is the Greenfinch UUID of the property. Worked call: ```bash curl -X POST "https://app.greenfinch.ai/api/v1/properties/3f2a9c1e-0000-4000-8000-000000000001/unlock" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "alreadyRevealed": false, "creditsCharged": 10 } } ``` ### GET /contacts/search Finds contacts by name or employer across everything visible to you (for example 'Dana at Lakeside'), without needing an id first. Returns name, title, employer and location; emails and phones stay behind the reveal tools. Scope: `data:read`. MCP twin: `greenfinch_find_contacts`. Cost: Free. Request query parameters: - `query` (string, required): Name or employer fragment, min 2 chars. - `limit` (integer): Max results, default 10, max 50. Worked call: ```bash curl "https://app.greenfinch.ai/api/v1/contacts/search?query=Lakeside&limit=10" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "contacts": [ { "id": "3f2a9c1e-0000-4000-8000-000000000201", "name": "Dana Whitfield", "title": "Facilities Manager", "employer": "Lakeside Property Services", "location": "Dallas, TX" } ], "note": null } } ``` ### GET /contacts/{id} Returns one contact's profile: title, role, seniority, employer, data-quality flags, and the properties they are linked to. Their name, email, phone, and LinkedIn URL are included only if your organization has already revealed this contact; otherwise the response shows the reveal price instead. Scope: `data:read`. MCP twin: `greenfinch_get_contact`. Cost: Never charges credits. If the contact is visible only because your plan covers its geography (not a paid per-contact reveal), each successful call counts against your organization's daily agent contact-reveal limit. Unrevealed contacts cost nothing and show reveal_price_credits (currently 5). Request: no body. `{id}` is the Greenfinch UUID of the contact. Worked call: ```bash curl "https://app.greenfinch.ai/api/v1/contacts/3f2a9c1e-0000-4000-8000-000000000041" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "contact_id": "3f2a9c1e-0000-4000-8000-000000000041", "title": "Senior Property Manager", "role_type": "property_management", "seniority": "manager", "employer_name": "Northgate Business Park LLC", "email_validated": true, "has_phone": true, "identity_not_confirmed": false, "revealed": true, "relationships": [ { "in_your_territory": true, "property_id": "3f2a9c1e-0000-4000-8000-000000000011", "address": "1200 Commerce Pkwy", "city": "Plano", "state": "TX" }, { "in_your_territory": false, "state": "OK", "county_fips": "40109", "coverage_request_tool": "greenfinch_request_coverage" } ], "fullName": "Dana Whitfield", "email": "dana.whitfield@example.com", "phone": "(972) 555-0142", "phoneLabel": "Direct", "phoneFallbackFromEmployer": false, "linkedinUrl": "https://www.linkedin.com/in/example-dana-whitfield" } } ``` ### POST /contacts/{id}/reveal Reveals one contact's details (name, email, phone, LinkedIn, title, employer) without unlocking a whole property. Use it when you want one specific person rather than everyone at a property. Scope: `data:unlock`. MCP twin: `greenfinch_reveal_contact`. Cost: 5 credits, unless the contact is already covered: you revealed it before, you unlocked one of its properties, or your plan covers its county. Covered contacts cost 0. Request: no body. `{id}` is the Greenfinch UUID of the contact. Worked call: ```bash curl -X POST "https://app.greenfinch.ai/api/v1/contacts/3f2a9c1e-0000-4000-8000-000000000011/reveal" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "contact": { "id": "3f2a9c1e-0000-4000-8000-000000000011", "fullName": "Dana Whitfield", "email": "dana.whitfield@example.com", "phone": "(214) 555-0142", "phoneLabel": "Direct", "phoneFallbackFromEmployer": false, "linkedinUrl": "https://www.linkedin.com/in/example-dana-whitfield", "title": "Director of Facilities", "employerName": "Northgate Business Park LLC" }, "creditsCharged": 5, "alreadyRevealed": false } } ``` ## Paging `POST /properties/search` pages in one of two ways, depending on how you search: - With `query` (free text): cursor paging only. Pass back `next_cursor` as `cursor` until `next_cursor` is null. `offset` is ignored. - With a location filter or `bounds`: offset paging only. `next_cursor` is always null here, even when `has_more` is true. Pass `next_offset` as `offset` until `next_offset` is null. `offset` can be at most 10,000. These searches also return `total_matching`. - A cursor works only with the sort it came from; otherwise the call is refused with `CURSOR_SORT_MISMATCH`. A malformed cursor is `INVALID_CURSOR`. Either way, start again from the first page. ## MCP connector - URL: `https://app.greenfinch.ai/api/v1/mcp`. It needs the Enterprise plan and a key with the `mcp` scope. - In an assistant that supports remote MCP connectors, add a custom connector with that URL. Where the assistant supports MCP sign-in, each person connects with their own Greenfinch account and the assistant acts as that person. - In a client you set up with JSON settings (for example `.mcp.json` or `mcp.json`), put an organization key in a header: ```json { "mcpServers": { "greenfinch": { "type": "http", "url": "https://app.greenfinch.ai/api/v1/mcp", "headers": { "Authorization": "Bearer ${GREENFINCH_API_KEY}" } } } } ``` - Call `tools/list` to read the tools and `tools/call` to run one. Send `Accept: application/json, text/event-stream`. A tool refusal comes back with HTTP `200`, `result.isError: true`, and the code in `result.structuredContent.code`. - An organization key acts for the whole organization: it can reveal contacts, spend credits and change the pipeline. A few tools need a person behind them and refuse organization keys. - Start a session with `greenfinch_describe_workspace`: it returns the counties you can see, categories, pipeline statuses, prices and limits. ## Refusal codes you will meet - Successful responses carry no rate-limit headers. They carry only `x-request-id` (An identifier for the request. Quote it when you contact support.). You learn a limit when a request is refused, so read the refusal code and follow the action given for it. - A request-rate refusal (`rate_limited`, `DAILY_REQUEST_LIMIT_EXCEEDED`) carries: - `Retry-After`: Seconds to wait before retrying. The same number is in `meta.retryAfterSeconds`. - `X-RateLimit-Limit`: The per-minute limit that was reached. - `X-RateLimit-Remaining`: Always `0` on a refusal. - `X-RateLimit-Reset`: When the window ends, in Unix seconds. - Every other `429` carries no `X-RateLimit-*` headers, and whether it carries `Retry-After` depends on the endpoint. `GET /leads/{id}` sends it for every limit, counting down to when the limit lifts. The search, read and unlock endpoints send it for the daily limits (`DAILY_CAP_EXCEEDED`, `CREDENTIAL_DAILY_CAP_EXCEEDED`, `ACCOUNT_DAILY_CAP_EXCEEDED`) but not for `RECORDS_ALLOWANCE_EXCEEDED`, and for `RATE_LIMITED` only when the refusal names a reset time. `GET /properties/{id}` never sends it. Every limit refusal says when it lifts in `meta.limit`: `resets_at` for a daily limit, `period_ends_at` for the records allowance. - Every limit refusal carries a `limit` object in `meta` naming the limit, its `level` (1 request rate, 2 daily data, 3 records allowance), its value, what was used and when it lifts (`resets_at`, or `period_ends_at` for the records allowance). - On `429` or `503`, wait for `Retry-After`, or until the time in `meta.limit`, then retry with backoff. Do not retry a `403` or `402`; tell the user what the code says. The codes a first integration meets most often. On REST the code is in `meta.code`, or top-level `code` for errors raised before the endpoint runs. On MCP a tool refusal puts it in `result.structuredContent.code`. "MCP" means the code comes only from a connector tool. The full reference lists all 76. - `SCOPE_NOT_GRANTED` (403): The key was not given the scope this call needs. Create a key with it. - `UPGRADE_REQUIRED` (403): Your plan does not include this feature; `feature` and `minimumTier` say which. Upgrade, or contact sales for Enterprise. - `rate_limited` (429): This key's per-minute request rate is used. Wait `Retry-After` seconds. Lower case, and different from `RATE_LIMITED`. - `INSUFFICIENT_CREDITS` (402 / MCP): Not enough credits; `required` and `available` give the numbers. The same code covers a member's personal monthly credit limit. - `DAILY_REQUEST_LIMIT_EXCEEDED` (429): The organization's daily request ceiling is used (level 1). Resume after the reset time in the limit object. - `ACCOUNT_DAILY_CAP_EXCEEDED` (429 / MCP): A Greenfinch account maximum for this kind of data is used (level 2). Your own admins cannot raise it. When the refusal carries `derived` `true` the number was worked out from your records allowance rather than typed in by anyone, and a larger allowance raises it; otherwise contact your Greenfinch contact. - `DAILY_CAP_EXCEEDED` (429 / MCP): The organization's daily limit for this action is used. Wait for `resetsAt`, or an admin raises it. - `CREDENTIAL_DAILY_CAP_EXCEEDED` (429 / MCP): This key's or connection's daily limit is used. - `RECORDS_ALLOWANCE_EXCEEDED` (429 / MCP): The request would take more records than remain in the records allowance (level 3). Looking in the app is unaffected. - `RECORDS_ALLOWANCE_NOT_SET` (409 / MCP): The organization's order has no records allowance recorded yet, so nothing can be taken. Waiting does not fix it and there is no `Retry-After`: contact Greenfinch. Changed 2026-09-16 from 429 to 409 on `GET /leads/{id}` and `GET /properties/{id}`; no retry fixes it. - `RATE_LIMITED` (429 / MCP): Too many requests of one kind (support, coverage or research requests). Slow down. Upper case, and different from the per-minute `rate_limited`. - `OUT_OF_TERRITORY` (403 / MCP): The item exists but is outside your service area (reason `subscription`) or the member's assigned territory (reason `territory`). The message names the county. - `NOT_FOUND` (404 / MCP): No such item visible to you. - `PROPERTY_RETIRED` (409 / MCP): The property was merged after a county data refresh. Use `successorPropertyId` from now on. - `PROPERTY_NOT_AVAILABLE` (404 / MCP): A property in the request is missing or outside your territory. - `INVALID_CURSOR` (400 / MCP): The paging cursor is malformed, or its sort value has the wrong type. Start again from the first page. - `CURSOR_SORT_MISMATCH` (400 / MCP): A cursor must be used with the sort it came from; a cursor in an older format also gets this. Start again from the first page. ## Identifiers - A property is identified only by its Greenfinch UUID. Store that UUID next to your own record and use it in every call. - Never identify a property by a county parcel number or any key you see in a URL. Only the UUID is supported, and anything else can change. - Firms, people, lists and webhook endpoints are also identified by UUIDs. In the lead bundle the identifiers are `greenfinch_property_id`, `organizations[].greenfinch_organization_id`, `contacts[].greenfinch_contact_id`, `pipeline.assigned_rep.greenfinch_user_id`. - When a property is merged after a county data refresh, calls return `409` `PROPERTY_RETIRED` with `successorPropertyId`. Replace the stored UUID with the successor. ## Do not - Do not work around a limit: no extra keys to multiply the rate, no parallel workers to beat a daily limit, no retry loops that ignore `Retry-After`. Wait, or ask an admin. - Do not share keys. Give each system its own key with only the scopes it needs, keep keys in a secrets manager, and never put one in client-side code, a repository or a prompt. - Do not scrape the Greenfinch app, or drive a browser signed in as a person to read pages. Use this API, within your allowance. - Do not store a property under anything but its Greenfinch UUID, and do not branch on error message text. - Do not spend credits the user did not ask for. Unlocking and revealing cost credits; check the cost first. # Part 2: Full reference ## 1. What Greenfinch is Greenfinch finds commercial properties and the people who own, manage and make decisions about them, for companies that sell services to those buildings. Your code can search those properties, firms and people, unlock their details and move leads through your pipeline. It can use the REST API or the MCP connector. ## 2. Base URLs - REST API: `https://app.greenfinch.ai/api/v1` - MCP connector: `https://app.greenfinch.ai/api/v1/mcp` (JSON-RPC 2.0 over HTTPS) - All requests use HTTPS. Request bodies are JSON with `Content-Type: application/json`. ## 3. Authentication, scopes and plans Send an organization API key as a bearer token on every request, REST and MCP alike: ```http Authorization: Bearer gfk_… ``` - An organization admin creates keys in the Greenfinch app under Org admin → Integrations. A key starts with `gfk_` and is shown once. Read it from an environment variable such as `GREENFINCH_API_KEY`; never put it in source code. - An organization can hold 10 active keys. - `GET /me` answers for any live key, whatever its scopes. Use it to check a key before anything else. - A key can call only what its scopes allow. A call without the scope is refused with `403` `SCOPE_NOT_GRANTED`. Scopes a key can hold: - `events:subscribe` (Team and Enterprise; write): Create and manage webhook subscriptions so qualified leads are sent to your CRM when they are qualified. - `leads:read_single` (Team and Enterprise; read): Read one qualified lead's full record at a time (metered the same as revealing it in the app). - `pipeline:write` (Team and Enterprise; write): Report won/lost outcomes from your CRM back into your Greenfinch pipeline. - `accounts:sync` (Team and Enterprise; write): Tell Greenfinch which properties are currently active or churned customers. - `account:read` (Team and Enterprise; read; must be ticked deliberately): Check this org's credit balance, lead-delivery allowance, and licensed coverage — the same numbers the app shows, for monitoring or gating your own automation. - `mcp` (Enterprise only; write; must be ticked deliberately): Let an autonomous AI agent use every Greenfinch connector tool for your org — search, reveal, qualify, and write back — subject to your agent settings and daily limits. - `events:read` (Team and Enterprise; read; must be ticked deliberately): Read the org's outbound lead-event feed (a lead.qualified event the first time a lead is qualified while External CRM mode is on; qualifying the same lead again sends nothing new) — the same events a webhook subscription delivers, available as a pull instead of a push. - `notes:read` (Team and Enterprise; read; must be ticked deliberately): Read the working notes your team has left on a property. - `lists:read` (Team and Enterprise; read; must be ticked deliberately): List your org's saved property/contact lists and their item counts. - `lists:write` (Team and Enterprise; write; must be ticked deliberately): Create a saved list and add properties or contacts to it. - `property:read` (Team and Enterprise; read; must be ticked deliberately): Look up one property's record (category, size, value, owner of record) and its research status by its Greenfinch id. Property facts only — never contacts. - `data:read` (Enterprise only; read; must be ticked deliberately): Search and look up properties, contacts, and organizations — the same results your team sees in the app. Looking at a contact your plan's coverage includes (but you haven't unlocked) counts against your organization's daily contact limit. - `data:unlock` (Enterprise only; write; must be ticked deliberately): Reveal a contact's details or unlock a property, spending credits exactly as it would in the app. Anything already unlocked is not charged again, and your daily limits apply. - `pipeline:transitions_full` (Enterprise, on a key that also holds `mcp`): lets `greenfinch_update_lead_status` do more. An organization admin turns it on for an existing key on the Integrations page; it cannot be chosen when the key is created. What each plan may call: - Team: the integration scopes above marked "Team and Enterprise" — webhooks, single lead bundles, pipeline write-back, customer-account sync, and reading events, notes, lists and property records. - Enterprise: everything in Team, plus `mcp`, `data:read`, `data:unlock`. - The REST search, read and unlock endpoints require the Enterprise plan and the `data:read` scope (`data:unlock` for unlocking and revealing). The plan is checked live on every call, never stored on the key. Any other plan gets a refusal, not a smaller set of endpoints: ```json // HTTP 403 { "success": false, "error": "API Access isn't included on your current plan. Contact sales for API access.", "code": "UPGRADE_REQUIRED", "feature": "apiAccess", "minimumTier": "enterprise" } ``` - The same live check covers account state: a suspended organization gets `403` `ORG_SUSPENDED`, and a closed account gets `403` `ACCOUNT_CLOSED`. ## 4. Looking and taking Looking is covered by the subscription. Taking a record draws on the allowance. Looking and taking are bounded differently: - A person reading a property, a firm or a person on screen in the Greenfinch app is doing what the plan already paid for, and is not rationed. - A record delivered to a program (over REST, through the connector, or to a webhook) counts as taken. Taken records are limited by the request rate, the daily data limits and, on Enterprise, the records allowance: how many distinct records may be taken in a billing period. - A search result is a summary (an identifier, an address, a one-line description, a count), so it never counts as taken and never draws the records allowance. Searching is limited only by the request rate. - Credits pay for seeing a record the organization is not already licensed for, and for research. Unlocking a property or revealing a contact spends credits; anything already unlocked is not charged again. ## 5. Main REST endpoints Every response uses one envelope. Success: ```json { "success": true, "data": { "…": "endpoint-specific" } } ``` Error: ```json { "success": false, "error": "Human-readable message", "meta": { "code": "MACHINE_CODE" } } ``` - Check the HTTP status first, then `success`. `data` is present only on success. - A `401`, and a `400` for bad arguments, carry no code: branch on the status. - Branch on the machine code, never on the message. Read `meta?.code ?? code`: errors raised before the endpoint runs carry the code at the top level. - On the search, read and unlock endpoints, `meta.issues` lists each problem as `{ field, message }`, and an unknown body key or query parameter is a `400`, not ignored. - Responses that carry data are sent with `Cache-Control: no-store`. Do not cache them. - Each search, read and unlock endpoint below names its MCP twin. It runs the same code as that tool and returns what the tool returns as `data`, with the same territory rules, charges and daily limits. ### POST /properties/search Searches commercial properties in one of three ways: free text (property name, address, city, ZIP code, the owner named on the county record, or the researched owner on properties you have unlocked or your plan covers), a location filter (county FIPS, ZIP, city or county name) plus any attribute filters, or a map bounding box. Use county or location mode to get a true match count and a category breakdown; use free text to find a specific property. Scope: `data:read`. MCP twin: `greenfinch_search_properties`. Cost: Free. Request body (JSON): - `query` (string): Free-text search across property name, address, city, ZIP code and owner: the owner named on the county record, and the researched owner on properties your organization has unlocked or your plan covers. The text is matched as typed: % and _ are ordinary characters, not wildcards. - `bounds` (object) - `bounds.minLat` (number, required) - `bounds.maxLat` (number, required) - `bounds.minLon` (number, required) - `bounds.maxLon` (number, required) - `filters` (object) - `filters.categories` (string[]) - `filters.subcategories` (string[]) - `filters.countyFips` (string[]): 5-digit county FIPS codes to search, e.g. ["51760"] for Richmond City, VA. Exact county scoping — use this instead of `bounds` whenever the question is about a county. - `filters.zipCodes` (string[]) - `filters.cities` (string[]) - `filters.counties` (string[]): State-qualified county slugs (e.g. "middlesex-nj") or legacy bare names. OR-combined with zipCodes/cities, then ANDed with every other filter — matches the app's location panel. - `filters.buildingClasses` (string[]) - `filters.acTypes` (string[]) - `filters.heatingTypes` (string[]) - `filters.roofTypes` (string[]) - `filters.exteriorWallTypes` (string[]) - `filters.poolTypes` (string[]) - `filters.fenceTypes` (string[]) - `filters.garageTypes` (string[]) - `filters.minLotSqft` (number) - `filters.maxLotSqft` (number) - `filters.minBuildingSqft` (number) - `filters.maxBuildingSqft` (number) - `filters.minUnits` (integer): Displayed unit count — a reviewed correction when one is in force (which may be no count), else the researched value when known, else the assessor's. - `filters.maxUnits` (integer) - `filters.minPropertyValue` (number) - `filters.maxPropertyValue` (number) - `filters.minYearBuilt` (number) - `filters.maxYearBuilt` (number) - `filters.minFloors` (integer) - `filters.maxFloors` (integer) - `filters.minBedrooms` (integer) - `filters.maxBedrooms` (integer) - `filters.minBathrooms` (number) - `filters.maxBathrooms` (number) - `filters.enrichmentStatus` ("researched" | "not_researched") - `filters.saleRecencyBuckets` (string (one of 5)[]): e.g. ["3_months", "over_a_year"] — sold or transferred within the last N (the newer of the Last Sold and Last Transfer dates). One of: 1_month, 3_months, 6_months, 12_months, over_a_year. - `filters.organizationId` (string): Properties linked to this organization. UUID. - `filters.contactId` (string): Properties linked to this contact. UUID. - `filters.changedKinds` ("ownership_transfer" | "sale" | "manager_change"[]): Only properties with a recorded change of one of these kinds: ownership_transfer (the county records a new owner, backed by a newer recorded transfer), sale (a newer sale date was recorded), manager_change (a later research pass found a different managing firm, judged a real change). Combine with changedWithinDays; call greenfinch_get_changes for the evidence. Available once change filters (signals) are switched on for Greenfinch; until then a call that passes this is refused with SIGNALS_NOT_ENABLED, never run without it. - `filters.changedWithinDays` (integer): Only changes the product detected within this many days (1 to 3650). Alone, it matches any of the kinds above. Available once change filters (signals) are switched on for Greenfinch; until then a call that passes this is refused with SIGNALS_NOT_ENABLED, never run without it. - `filters.leaseTypes` ("triple_net" | "gross" | "modified_gross" | "ground_lease"[]): Only properties whose researched lease arrangement is one of these (triple_net = Triple-net, gross = Gross, modified_gross = Modified gross, ground_lease = Ground lease). Owner-occupied and sale-leaseback are not filterable: they describe who owns the property and are shown on its detail once its research is unlocked. Research records a lease type only when a source it read states it (a leasing flyer, listing or article), so most properties have none and never match; the property's detail carries the source. Available once the lease-type field is switched on for Greenfinch; until then a call that passes this is refused with LEASE_TYPE_NOT_ENABLED, never run without it. - `limit` (integer): Max results, default 50, max 200. - `cursor` (string): Pagination cursor from a prior `query`-mode call. - `offset` (integer): Rows to skip in a county/bounds search — page 2 of a ranked list is offset=limit. (min 0, max 10000) - `sortBy` (string (one of 19)): Every sort the app's property search offers — works in query mode AND county/bounds mode. Defaults to "relevance". Numeric and date sorts place properties with no value LAST in both directions; the text sorts (commonName, ownerName, address, city, owner) treat a missing value as the highest value, so it sorts last ascending and FIRST descending. "owner" sorts the owner of record as the county states it; "ownerName" sorts the assessor's owner name, a separate value. "address" sorts the county's address line, not the tidied address shown in results. One of: relevance, commonName, lotSqft, buildingSqft, totalUnits, contactCount, landscapableSqft, roofAreaSqft, buildingFootprintSqft, yearBuilt, numFloors, ownerName, propertyValue, updatedAt, lastEnrichedAt, lastSaleDate, address, city, owner. - `sortOrder` ("asc" | "desc") - `verbosity` ("compact" | "full"): compact (default): the decision fields — id, address and location, owner, category, sizes, value, year, a short description. full: adds the county account numbers of a multi-parcel property, grouping flags and the assessor's owner-name variants. Compact rows are about 40% smaller; prefer compact unless you need those. Worked call: ```bash curl -X POST "https://app.greenfinch.ai/api/v1/properties/search" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filters": { "countyFips": [ "48085" ], "categories": [ "Office" ], "minBuildingSqft": 20000 }, "sortBy": "buildingSqft", "sortOrder": "desc", "limit": 2 }' ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "results": [ { "id": "3f2a9c1e-0000-4000-8000-000000000001", "address": "1200 Commerce Pkwy", "city": "Plano", "state": "TX", "zip": "75074", "county": "Collin", "lat": 33.0198, "lon": -96.6989, "owner": "Northgate Business Park LLC", "commonName": "Northgate Business Park", "assetCategory": "Office", "assetSubcategory": "Office Building", "totalParval": 18450000, "yearBuilt": 2004, "lotSqft": 217800, "buildingSqft": 96000, "numFloors": 4, "aiRationale": "Four-story multi-tenant office building on a landscaped campus with surface parking...", "enrichmentStatus": "completed" } ], "total_matching": 214, "has_more": true, "returned": 1, "offset": 0, "next_offset": 1, "next_cursor": null, "category_breakdown": [ { "category": "Office", "count": 214, "withValue": 209, "totalValue": 1934500000 } ], "sorted_by": { "field": "buildingSqft", "direction": "desc" }, "searched_scope": { "scoped": true, "countyCount": 2, "states": ["TX"] }, "truncation_note": "Showing 1 of 214 matches. Ranked by buildingSqft (desc); pass offset=1 for the next page. Use total_matching, never the row count, when reporting how many exist." } } ``` ### GET /properties/{id}/detail Fetches one property's full detail: address, size, age, category, assessed value, ownership and management (when unlocked), the full AI write-up, and its contacts. Read researchState first: it tells you whether to start research, unlock the property, or read the contacts directly. Scope: `data:read`. MCP twin: `greenfinch_get_property`. Cost: No credits. Contacts your organization can see only through its geographic subscription (not through a purchase) count against the daily contact-disclosure limit; if that limit is used up they come back masked instead. Request: no body. `{id}` is the Greenfinch UUID of the property. Worked call: ```bash curl "https://app.greenfinch.ai/api/v1/properties/3f2a9c1e-0000-4000-8000-000000000001/detail" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "property": { "id": "3f2a9c1e-0000-4000-8000-000000000001", "address": "1200 Commerce Pkwy", "city": "Plano", "state": "TX", "zip": "75074", "county": "Collin", "lat": 33.0198, "lon": -96.6989, "lotSqft": 217800, "buildingSqft": 96000, "yearBuilt": 2004, "yearBuiltDetail": { "basis": "per_building", "oldest": 1998, "newest": 2004, "primaryBuildingBasis": "area", "buildings": 3, "buildingsTotal": 3 }, "numFloors": 4, "numFloorsDetail": { "basis": "per_building", "max": 4, "areaWeightedMean": 3.51, "primaryBuilding": 4, "primaryBuildingBasis": "area", "buildings": 3, "buildingsTotal": 3 }, "effectiveYearBuilt": 2015, "effectiveYearBuiltDetail": { "basis": "per_building", "oldest": 2009, "newest": 2015, "primaryBuildingBasis": "area", "buildings": 2, "buildingsTotal": 3 }, "heatingType": "Rooftop Package Unit", "heatingTypeDetail": { "basis": "per_building", "buildings": 3, "buildingsTotal": 3, "areaShare": 1, "values": [ { "value": "Rooftop Package Unit", "buildings": 2, "areaShare": 0.844 }, { "value": "Heat Pump", "buildings": 1, "areaShare": 0.156 } ] }, "coolingType": "Rooftop Package Unit", "coolingTypeDetail": { "basis": "per_building", "buildings": 3, "buildingsTotal": 3, "areaShare": 1, "values": [ { "value": "Rooftop Package Unit", "buildings": 2, "areaShare": 0.844 }, { "value": "Heat Pump", "buildings": 1, "areaShare": 0.156 } ] }, "primaryConstructionType": "Concrete", "primaryConstructionTypeDetail": { "basis": "per_building", "buildings": 3, "buildingsTotal": 3, "areaShare": 1, "values": [{ "value": "Concrete", "buildings": 3, "areaShare": 1 }] }, "exteriorWallMaterial": null, "exteriorWallMaterialDetail": null, "foundationType": "Slab on Grade", "foundationTypeDetail": { "basis": "per_building", "buildings": 3, "buildingsTotal": 3, "areaShare": 1, "values": [{ "value": "Slab on Grade", "buildings": 3, "areaShare": 1 }] }, "roofCover": null, "roofCoverDetail": null, "roofShape": null, "roofShapeDetail": null, "qualityGrade": "Good", "qualityGradeDetail": { "basis": "per_building", "buildings": 3, "buildingsTotal": 3, "areaShare": 1, "values": [ { "value": "Good", "buildings": 2, "areaShare": 0.844 }, { "value": "Average", "buildings": 1, "areaShare": 0.156 } ] }, "hasPool": false, "hasPoolDetail": { "basis": "per_building", "count": 0, "buildings": 3, "buildingsTotal": 3 }, "hasSprinklers": true, "hasSprinklersDetail": { "basis": "per_building", "count": 2, "buildings": 2, "buildingsTotal": 3 }, "buildingAttributesBasis": "per_building", "footprintArea": 21500, "footprintAreaSource": "measured", "assetCategory": "Office", "assetSubcategory": "Office Building", "commonName": "Northgate Business Park", "nameLocked": false, "beneficialOwner": "Northgate Holdings Group", "beneficialOwnerType": "private_investor", "managementCompany": "Lakeside Property Services", "aiRationale": "Four-story multi-tenant office building on a landscaped campus. Managed by Lakeside Property Services on behalf of Northgate Holdings Group...", "enrichmentStatus": "completed", "lastEnrichedAt": "2026-08-20T14:11:00.000Z", "researchedAt": "2026-08-20T14:11:00.000Z", "researchTier": { "displayName": "Base research model", "description": "Our standard AI research pass — fast and accurate for the large majority of properties.", "isUpgradeAvailable": true }, "totalParval": 18450000, "premiumDataRedacted": false }, "contacts": [ { "id": "3f2a9c1e-0000-4000-8000-000000000201", "fullName": "Dana Whitfield", "normalizedName": "dana whitfield", "contactType": "individual", "email": "dana.whitfield@example.com", "normalizedEmail": "dana.whitfield@example.com", "emailStatus": "valid", "emailValidationStatus": "valid", "alternateEmails": [], "phone": "(214) 555-0142", "normalizedPhone": "+12145550142", "phoneLabel": "Direct", "phoneExtension": null, "title": "Facilities Manager", "companyDomain": "example.com", "employerName": "Lakeside Property Services", "roleChangedAt": null, "employerChangedAt": null, "likelyRoleChangeAt": null, "likelyEmployerChangeAt": null, "currentRoleStartedAt": "2022-03-01T00:00:00.000Z", "currentEmployerStartedAt": "2019-06-01T00:00:00.000Z", "linkedinUrl": "https://www.linkedin.com/in/example-dana-whitfield", "linkedinStatus": "verified", "location": "Dallas, TX", "enrichedAt": "2026-08-20T14:11:00.000Z", "createdAt": "2026-08-20T14:11:00.000Z", "role": "property_manager", "relationshipStatus": "active", "revealed": true, "…": "…" } ], "contactsSummary": null, "researchRevealed": true, "researchState": "revealed", "revealSource": "paid", "coveredByPlan": true } } ``` ### POST /properties/{id}/unlock Unlocks a researched property for your organization, which gives you its full research and every contact attached to it. Use it before reading a property's contacts or fetching its lead bundle. Scope: `data:unlock`. MCP twin: `greenfinch_reveal_property`. Cost: 10 credits to unlock the property, unless your organization already unlocked it or your plan covers its county. Retrying is safe: a property that is already unlocked costs 0. Request: no body. `{id}` is the Greenfinch UUID of the property. Worked call: ```bash curl -X POST "https://app.greenfinch.ai/api/v1/properties/3f2a9c1e-0000-4000-8000-000000000001/unlock" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "alreadyRevealed": false, "creditsCharged": 10 } } ``` ### POST /contacts/browse Finds contacts in your territory by role type, job title, employer name, firm, county, or whether you have already revealed them. Rows describe the person (title, role, seniority, employer, property count) without ever including their name, email, phone, or LinkedIn URL; use greenfinch_get_contact or greenfinch_reveal_contact for those. Where switched on, a firm's people can also be ranked by how well Greenfinch already knows them in one county's market. Scope: `data:read`. MCP twin: `greenfinch_browse_contacts`. Cost: Free. Does not spend credits or any daily limit. Request body (JSON): - `roleType` (string): Contact role-type taxonomy key (see contact-role-taxonomy). - `titleText` (string): Substring match on the contact's title. - `countyFips` (string[]): 5-digit county FIPS codes, matched via the contact's attached properties. - `revealedState` ("revealed" | "locked" | "any"): Default any. - `employerText` (string): Substring match on the contact's employer name. - `organizationId` (string): Organization UUID: only that firm's people (a current employment record at the firm, or, with marketCountyFips, anyone the market ranking ties to the firm). - `marketCountyFips` (string): 5-digit county FIPS, with organizationId: mark and rank the firm's people Greenfinch already knows in this county's market. - `sort` ("propertyCount" | "title" | "employerName" | "marketRank"): Default employerName. marketRank needs organizationId and marketCountyFips: strongest market evidence first, then everyone else. - `limit` (integer): Max rows, default 25, max 200. - `offset` (integer): Rows to skip. - `changedKinds` ("contact_departure" | "contact_title_change" | "contact_employer_change"[]): Only people with a recorded change of one of these kinds: contact_departure (the person left the employer we showed for them), contact_title_change (their title changed, seen on their own public listing or our re-check of their profile), contact_employer_change (our re-check of their profile shows a different employer). Combine with changedWithinDays; call greenfinch_get_changes for the evidence. Available once change filters (signals) are switched on for Greenfinch; until then a call that passes this is refused with SIGNALS_NOT_ENABLED, never run without it. - `changedWithinDays` (integer): Only changes the product detected within this many days (1 to 3650). Alone, it matches any of the kinds above. Available once change filters (signals) are switched on for Greenfinch; until then a call that passes this is refused with SIGNALS_NOT_ENABLED, never run without it. Worked call: ```bash curl -X POST "https://app.greenfinch.ai/api/v1/contacts/browse" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "roleType": "property_management", "titleText": "manager", "countyFips": [ "48085" ], "revealedState": "locked", "employerText": "Northgate", "sort": "propertyCount", "limit": 25, "offset": 0 }' ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "contacts": [ { "contact_id": "3f2a9c1e-0000-4000-8000-000000000041", "title": "Senior Property Manager", "role_type": "property_management", "seniority": "manager", "employer_name": "Northgate Business Park LLC", "property_count_in_territory": 5, "revealed": false, "email_validated": true, "has_phone": true, "identity_not_confirmed": false }, { "contact_id": "3f2a9c1e-0000-4000-8000-000000000042", "title": "Regional Property Manager", "role_type": "property_management", "seniority": "director", "employer_name": "Northgate Business Park LLC", "property_count_in_territory": 3, "revealed": false, "email_validated": false, "has_phone": false, "identity_not_confirmed": false } ], "total_matching": 2 } } ``` ### GET /contacts/search Finds contacts by name or employer across everything visible to you (for example 'Dana at Lakeside'), without needing an id first. Returns name, title, employer and location; emails and phones stay behind the reveal tools. Scope: `data:read`. MCP twin: `greenfinch_find_contacts`. Cost: Free. Request query parameters: - `query` (string, required): Name or employer fragment, min 2 chars. - `limit` (integer): Max results, default 10, max 50. Worked call: ```bash curl "https://app.greenfinch.ai/api/v1/contacts/search?query=Lakeside&limit=10" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "contacts": [ { "id": "3f2a9c1e-0000-4000-8000-000000000201", "name": "Dana Whitfield", "title": "Facilities Manager", "employer": "Lakeside Property Services", "location": "Dallas, TX" } ], "note": null } } ``` ### GET /contacts/{id} Returns one contact's profile: title, role, seniority, employer, data-quality flags, and the properties they are linked to. Their name, email, phone, and LinkedIn URL are included only if your organization has already revealed this contact; otherwise the response shows the reveal price instead. Scope: `data:read`. MCP twin: `greenfinch_get_contact`. Cost: Never charges credits. If the contact is visible only because your plan covers its geography (not a paid per-contact reveal), each successful call counts against your organization's daily agent contact-reveal limit. Unrevealed contacts cost nothing and show reveal_price_credits (currently 5). Request: no body. `{id}` is the Greenfinch UUID of the contact. Worked call: ```bash curl "https://app.greenfinch.ai/api/v1/contacts/3f2a9c1e-0000-4000-8000-000000000041" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "contact_id": "3f2a9c1e-0000-4000-8000-000000000041", "title": "Senior Property Manager", "role_type": "property_management", "seniority": "manager", "employer_name": "Northgate Business Park LLC", "email_validated": true, "has_phone": true, "identity_not_confirmed": false, "revealed": true, "relationships": [ { "in_your_territory": true, "property_id": "3f2a9c1e-0000-4000-8000-000000000011", "address": "1200 Commerce Pkwy", "city": "Plano", "state": "TX" }, { "in_your_territory": false, "state": "OK", "county_fips": "40109", "coverage_request_tool": "greenfinch_request_coverage" } ], "fullName": "Dana Whitfield", "email": "dana.whitfield@example.com", "phone": "(972) 555-0142", "phoneLabel": "Direct", "phoneFallbackFromEmployer": false, "linkedinUrl": "https://www.linkedin.com/in/example-dana-whitfield" } } ``` ### GET /contacts/{id}/relationships Lists every property a contact is attached to. Properties inside your territory come back with id and address; properties outside it appear only as a state and county, with a pointer to request coverage. Scope: `data:read`. MCP twin: `greenfinch_get_contact_relationships`. Cost: Free. Request: no body. `{id}` is the Greenfinch UUID of the contact. Worked call: ```bash curl "https://app.greenfinch.ai/api/v1/contacts/3f2a9c1e-0000-4000-8000-000000000201/relationships" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "relationships": [ { "in_your_territory": true, "property_id": "3f2a9c1e-0000-4000-8000-000000000001", "address": "1200 Commerce Pkwy", "city": "Plano", "state": "TX" }, { "in_your_territory": false, "state": "OK", "county_fips": "40109", "coverage_request_tool": "greenfinch_request_coverage" } ] } } ``` ### POST /contacts/{id}/reveal Reveals one contact's details (name, email, phone, LinkedIn, title, employer) without unlocking a whole property. Use it when you want one specific person rather than everyone at a property. Scope: `data:unlock`. MCP twin: `greenfinch_reveal_contact`. Cost: 5 credits, unless the contact is already covered: you revealed it before, you unlocked one of its properties, or your plan covers its county. Covered contacts cost 0. Request: no body. `{id}` is the Greenfinch UUID of the contact. Worked call: ```bash curl -X POST "https://app.greenfinch.ai/api/v1/contacts/3f2a9c1e-0000-4000-8000-000000000011/reveal" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "contact": { "id": "3f2a9c1e-0000-4000-8000-000000000011", "fullName": "Dana Whitfield", "email": "dana.whitfield@example.com", "phone": "(214) 555-0142", "phoneLabel": "Direct", "phoneFallbackFromEmployer": false, "linkedinUrl": "https://www.linkedin.com/in/example-dana-whitfield", "title": "Director of Facilities", "employerName": "Northgate Business Park LLC" }, "creditsCharged": 5, "alreadyRevealed": false } } ``` ### POST /organizations/browse Lists the owner and property-management firms that hold at least one property in your visible territory. Use it to find firms by name, role, county, or portfolio size before drilling into one with greenfinch_get_organization. Scope: `data:read`. MCP twin: `greenfinch_browse_organizations`. Cost: Free. Does not spend credits or any daily limit. Request body (JSON): - `role` (string (one of 5)): One of: owner, property_manager, operator, tenant, any. - `countyFips` (string[]): 5-digit county FIPS codes. - `query` (string): Substring match on the firm's name. - `minProperties` (integer): Only firms with at least this many in-territory properties. - `sort` ("propertyCount" | "name"): Default name. - `limit` (integer): Max rows, default 25, max 200. - `offset` (integer): Rows to skip. Worked call: ```bash curl -X POST "https://app.greenfinch.ai/api/v1/organizations/browse" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "role": "owner", "countyFips": [ "48085" ], "query": "Northgate", "minProperties": 2, "sort": "propertyCount", "limit": 25, "offset": 0 }' ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "organizations": [ { "organization_id": "3f2a9c1e-0000-4000-8000-000000000001", "name": "Northgate Business Park LLC", "domain": "example.com", "roles": ["owner", "property_manager"], "property_count_in_territory": 7, "researched": true }, { "organization_id": "3f2a9c1e-0000-4000-8000-000000000002", "name": "Northgate Retail Holdings LP", "domain": null, "roles": ["owner"], "property_count_in_territory": 3, "researched": false } ], "total_matching": 2, "scope": { "scoped": true, "countyCount": 4, "states": ["TX"] } } } ``` ### GET /organizations/{id} Returns one firm's detail: name, domain, roles, social handles, how many of its properties you can see (and how many are researched or revealed), its first few properties, and a count of its contacts by role type. Where switched on, it can also count the firm's people Greenfinch already knows managing properties in one county's market. Use it after finding a firm with greenfinch_browse_organizations; it never returns contact names. Scope: `data:read`. MCP twin: `greenfinch_get_organization`. Cost: Free. Does not spend credits or any daily limit. Request: no body. `{id}` is the Greenfinch UUID of the organization. Worked call: ```bash curl "https://app.greenfinch.ai/api/v1/organizations/3f2a9c1e-0000-4000-8000-000000000001" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "organization": { "organization_id": "3f2a9c1e-0000-4000-8000-000000000001", "name": "Northgate Business Park LLC", "domain": "example.com", "roles": ["owner", "property_manager"], "socials": { "linkedin": "northgate-business-park-example", "twitter": null, "facebook": null, "crunchbase": null } }, "property_count_in_territory": 7, "researched_property_count": 5, "revealed_property_count": 2, "properties": [ { "property_id": "3f2a9c1e-0000-4000-8000-000000000011", "address": "1200 Commerce Pkwy", "city": "Plano", "state": "TX", "role": "owner" }, { "property_id": "3f2a9c1e-0000-4000-8000-000000000012", "address": "1250 Commerce Pkwy", "city": "Plano", "state": "TX", "role": "property_manager" } ], "properties_truncated": false, "contacts_by_role": [ { "role_type": "property_management", "count": 4 }, { "role_type": "other_unclassified", "count": 1 } ] } } ``` ### GET /organizations/{id}/properties Lists every property an owner or property-management company is connected to, with its role on each property. Properties outside your territory appear only as a state and county, never as an id or address. Scope: `data:read`. MCP twin: `greenfinch_get_organization_properties`. Cost: Free. Request: no body. `{id}` is the Greenfinch UUID of the organization. Worked call: ```bash curl "https://app.greenfinch.ai/api/v1/organizations/3f2a9c1e-0000-4000-8000-000000000401/properties" \ -H "Authorization: Bearer $GREENFINCH_API_KEY" ``` Response (200, illustrative values; "…" marks a shortened part): ```json { "success": true, "data": { "organization": { "id": "3f2a9c1e-0000-4000-8000-000000000401", "name": "Lakeside Property Services" }, "properties": [ { "in_your_territory": true, "property_id": "3f2a9c1e-0000-4000-8000-000000000001", "address": "1200 Commerce Pkwy", "city": "Plano", "state": "TX", "role": "property_manager", "attributionType": "named_for_property" }, { "in_your_territory": false, "state": "OK", "county_fips": "40109", "coverage_request_tool": "greenfinch_request_coverage", "role": "owner", "attributionType": "county_record" } ] } } ``` ### Change notification: events and webhooks These endpoints have no MCP twin. Their `400` answers carry a message and no `meta.issues`. `GET /events` (scope `events:read`) returns the organization's recent lead events, newest first. The connector tool `greenfinch_get_emitted_events` reads the same events. A `limit` above its maximum is reduced to the maximum, not refused. Query parameters: - `days` (integer): How many days back (default 30, max 90). - `limit` (integer): Max rows, default 50, max 200. - `eventType` (string (one of 9)): Only events of this type. Omit to see every type. The signal.* types work once signals are switched on. One of: lead.qualified, lead.changed, signal.ownership_transfer, signal.sale, signal.manager_change, signal.contact_departure, signal.contact_title_change, signal.contact_employer_change, signal.withdrawn. `POST /webhooks` (scope `events:subscribe`) registers a public `https://` endpoint. Body: `{ "url": "https://…", "events": [...] }`. Events you can subscribe to: `lead.qualified`, `signal.ownership_transfer`, `signal.sale`, `signal.manager_change`, `signal.contact_departure`, `signal.contact_title_change`, `signal.contact_employer_change`, `signal.withdrawn`. It answers `201` with `data.endpoint`, `data.signing_secret` and `data.superseded_endpoint_id`. The signing secret is shown only in this response, so store it. Subscribing the same address again replaces the endpoint, issues a new secret, and names the replaced endpoint in `superseded_endpoint_id`. `DELETE /webhooks/{id}` removes an endpoint. Each delivery is an HTTPS `POST` to your address: - The JSON body has these top-level fields: `event_id`, `event_type`, `created_at`, `test`, `data`. `data` is the lead bundle (section 8). `test` is `true` only for a test event, so ignore those in production. - Headers: `Greenfinch-Signature: t=,v1=`, `Greenfinch-Event-Id` (the same as `event_id`; use it to ignore a repeated delivery) and `Greenfinch-Event-Type`. - To verify: compute HMAC-SHA256, keyed with your signing secret, over `.`, where `` is the `t` value and `` is the request body exactly as received, before any JSON parsing. Compare the hex result with `v1` in constant time, and reject a `t` more than 5 minutes old. - Answer with any `2xx` status. A redirect is not followed. ### Every REST endpoint and what it needs Request and response shapes for the endpoints not shown above are at https://www.greenfinch.ai/developers/rest. - `GET /account`: scope `account:read`, Team and Enterprise. Returns your organization's credit balance, lead-delivery allowance and licensed coverage. - `GET /allowance`: any live key, Enterprise only. Returns the records allowance and daily limits in force for your organization, and how much of each is used. - `GET /contacts/{id}`: scope `data:read`, Enterprise only. Returns one contact's profile: title, role, seniority, employer, data-quality flags, and the properties they are linked to. - `GET /contacts/{id}/relationships`: scope `data:read`, Enterprise only. Lists every property a contact is attached to. - `POST /contacts/{id}/reveal`: scope `data:unlock`, Enterprise only. Reveals one contact's details (name, email, phone, LinkedIn, title, employer) without unlocking a whole property. - `POST /contacts/browse`: scope `data:read`, Enterprise only. Finds contacts in your territory by role type, job title, employer name, firm, county, or whether you have already revealed them. - `GET /contacts/search`: scope `data:read`, Enterprise only. Finds contacts by name or employer across everything visible to you (for example 'Dana at Lakeside'), without needing an id first. - `POST /customer-accounts/sync`: scope `accounts:sync`, Team and Enterprise. Marks properties as your organization's active or churned customer accounts. - `GET /events`: scope `events:read`, Team and Enterprise. Returns your organization's recent lead events, newest first. - `GET /leads/{id}`: scope `leads:read_single`, Team and Enterprise. Returns one qualified lead's full lead bundle. - `POST /leads/{id}/status`: scope `pipeline:write`, Team and Enterprise. Records a won or lost outcome from your CRM on a lead in your Greenfinch pipeline. - `GET /lists`: scope `lists:read`, Team and Enterprise. Lists your organization's saved lists with their item counts. - `POST /lists`: scope `lists:write`, Team and Enterprise. Creates a saved list of properties or contacts. - `POST /lists/{id}/items`: scope `lists:write`, Team and Enterprise. Adds properties or contacts to an existing saved list. - `GET /me`: any live key, any plan. Checks a key: returns the organization it belongs to and the scopes it holds. - `GET /notes`: scope `notes:read`, Team and Enterprise. Returns the notes your team left on one property. - `GET /organizations/{id}`: scope `data:read`, Enterprise only. Returns one firm's detail: name, domain, roles, social handles, how many of its properties you can see (and how many are researched or revealed), its first few properties, and a count of its contacts by role type. - `GET /organizations/{id}/properties`: scope `data:read`, Enterprise only. Lists every property an owner or property-management company is connected to, with its role on each property. - `POST /organizations/browse`: scope `data:read`, Enterprise only. Lists the owner and property-management firms that hold at least one property in your visible territory. - `GET /properties/{id}`: scope `property:read`, Team and Enterprise. Returns one property's record and research status. Never returns contacts. - `GET /properties/{id}/detail`: scope `data:read`, Enterprise only. Fetches one property's full detail: address, size, age, category, assessed value, ownership and management (when unlocked), the full AI write-up, and its contacts. - `POST /properties/{id}/unlock`: scope `data:unlock`, Enterprise only. Unlocks a researched property for your organization, which gives you its full research and every contact attached to it. - `POST /properties/search`: scope `data:read`, Enterprise only. Searches commercial properties in one of three ways: free text (property name, address, city, ZIP code, the owner named on the county record, or the researched owner on properties you have unlocked or your plan covers), a location filter (county FIPS, ZIP, city or county name) plus any attribute filters, or a map bounding box. - `POST /webhooks`: scope `events:subscribe`, Team and Enterprise. Registers a webhook endpoint for lead events. - `DELETE /webhooks/{id}`: scope `events:subscribe`, Team and Enterprise. Removes a webhook endpoint. ## 6. MCP connector - URL: `https://app.greenfinch.ai/api/v1/mcp`. It needs the Enterprise plan and a key with the `mcp` scope. - In an assistant that supports remote MCP connectors, add a custom connector with that URL. Where the assistant supports MCP sign-in, each person connects with their own Greenfinch account and the assistant acts as that person. - In a client you set up with JSON settings (for example `.mcp.json` or `mcp.json`), put an organization key in a header: ```json { "mcpServers": { "greenfinch": { "type": "http", "url": "https://app.greenfinch.ai/api/v1/mcp", "headers": { "Authorization": "Bearer ${GREENFINCH_API_KEY}" } } } } ``` - Call `tools/list` to read the tools and `tools/call` to run one. Send `Accept: application/json, text/event-stream`. A tool refusal comes back with HTTP `200`, `result.isError: true`, and the code in `result.structuredContent.code`. - An organization key acts for the whole organization: it can reveal contacts, spend credits and change the pipeline. A few tools need a person behind them and refuse organization keys. - Start a session with `greenfinch_describe_workspace`: it returns the counties you can see, categories, pipeline statuses, prices and limits. Tools (77): ### Search and discovery - `greenfinch_search_properties`: Searches commercial properties in one of three ways: free text (property name, address, city, ZIP code, the owner named on the county record, or the researched owner on properties you have unlocked or your plan covers), a location filter (county FIPS, ZIP, city or county name) plus any attribute filters, or a map bounding box. - `greenfinch_list_saved_searches` (needs a per-user connection): Lists the connected user's saved property searches, the same ones shown in the app's search picker. - `greenfinch_save_search` (writes; needs a per-user connection): Creates or updates one of the connected user's saved searches, written in the app's own format so it appears in their in-app search picker. - `greenfinch_run_saved_search` (needs a per-user connection): Runs one of the connected user's saved searches and returns matching properties with a true total and category breakdown. - `greenfinch_delete_saved_search` (writes; needs a per-user connection): Deletes one of the connected user's own saved searches, the same as deleting it from the app's search picker. - `greenfinch_get_changes`: Lists recorded changes (signals) newest first: a property changing hands, a newer recorded sale, a different managing firm, a person leaving the employer we showed, or a changed title or employer. ### Properties - `greenfinch_get_property` (draws a daily limit): Fetches one property's full detail: address, size, age, category, assessed value, ownership and management (when unlocked), the full AI write-up, and its contacts. - `greenfinch_reveal_property` (writes; can cost credits): Unlocks a researched property for your organization, which gives you its full research and every contact attached to it. - `greenfinch_get_property_constituents`: For a complex property made up of several parcels (for example an office campus), lists the individual sub-parcels with address, size, category, and assessed value where your plan allows. ### Contacts and organizations - `greenfinch_get_contact_relationships`: Lists every property a contact is attached to. - `greenfinch_get_organization_properties`: Lists every property an owner or property-management company is connected to, with its role on each property. - `greenfinch_find_contacts`: Finds contacts by name or employer across everything visible to you (for example 'Dana at Lakeside'), without needing an id first. - `greenfinch_reveal_contact` (writes; can cost credits; draws a daily limit): Reveals one contact's details (name, email, phone, LinkedIn, title, employer) without unlocking a whole property. - `greenfinch_browse_organizations`: Lists the owner and property-management firms that hold at least one property in your visible territory. - `greenfinch_get_organization`: Returns one firm's detail: name, domain, roles, social handles, how many of its properties you can see (and how many are researched or revealed), its first few properties, and a count of its contacts by role type. - `greenfinch_browse_contacts`: Finds contacts in your territory by role type, job title, employer name, firm, county, or whether you have already revealed them. - `greenfinch_get_contact` (draws a daily limit): Returns one contact's profile: title, role, seniority, employer, data-quality flags, and the properties they are linked to. ### Research - `greenfinch_research_contact` (writes; can cost credits; draws a daily limit): Returns a contact's researched details. - `greenfinch_research_organization` (writes; can cost credits): Looks up a company by name and/or website domain and returns its profile: industry, headcount, founding year, location, LinkedIn and logo. - `greenfinch_deep_research_contact` (writes; can cost credits): Starts a background Deep Research run on a contact: a fresh live lookup of their details plus web research that produces a condensed outreach summary. - `greenfinch_deep_research_status`: Returns the progress of a contact's Deep Research run and, once it finishes, a condensed outreach summary: a one-line reason to reach out, a few talking points, and the contact's title and employer. - `greenfinch_draft_email` (writes; can cost credits; needs a per-user connection): Writes a first-touch outreach email (subject and body) for a contact, based on the contact's conversation strategy and your organization's email templates. - `greenfinch_start_research` (writes; can cost credits): Starts AI research on a batch of properties: given property ids, or every property on a saved list. - `greenfinch_research_status`: Reports progress on a research batch started with greenfinch_start_research: the overall status, completed/failed/active counts, and a status for each property. - `greenfinch_get_research_history`: Lists your organization's past research runs in two separately paged sections: property research batches and contact Deep Research runs, each with status and credits. - `greenfinch_price_research`: Quotes what a research launch would cost without launching or charging anything, for a batch of properties, an organization lookup, or a contact Deep Research run. - `greenfinch_refresh_research` (writes; can cost credits): Re-runs AI research on a property that has already been researched, to pick up newer information, for 10 credits whatever the property. - `greenfinch_cancel_research` (writes; needs a per-user connection): Stops a property-research batch you personally started while it is still running, refunding the properties it had not processed yet. ### Lists - `greenfinch_list_lists`: Lists your saved property or contact lists with their item counts. - `greenfinch_get_list`: Reads a saved list's members. - `greenfinch_create_list` (writes): Creates a new saved list that holds either properties or contacts. - `greenfinch_add_to_list` (writes): Adds up to 500 properties or contacts, by id, to an existing saved list. - `greenfinch_remove_from_list` (writes): Removes one property or contact from a saved list. - `greenfinch_rename_list` (writes): Gives a saved list a new name. - `greenfinch_set_list_visibility` (writes): Shares a saved list with the whole team ('team') or makes it private again ('private'). - `greenfinch_add_search_results_to_list` (writes): Runs a property search with the same filters as greenfinch_search_properties and adds the matching properties you can currently see to a saved properties list, up to 500 per call. - `greenfinch_get_list_provenance`: Shows who added each item on a saved list and when, plus who created the list. ### Pipeline and CRM - `greenfinch_get_pipeline`: Shows where properties stand in your organization's sales pipeline: status, deal value, when it changed, and (when you pass property ids) which ones nobody has worked yet. - `greenfinch_add_note` (writes; needs a per-user connection): Writes a note on a property, the working record of a lead (for example 'called, decisions renew in March'). - `greenfinch_get_notes`: Reads your organization's notes on a property, newest first, with author names. - `greenfinch_qualify_lead` (writes; draws a daily limit): Moves a property's deal into the 'qualified' stage with a dollar deal value. - `greenfinch_disqualify_lead` (writes; draws a daily limit): Marks a property's deal as disqualified, with a written reason and optionally one of the org's reason codes, for a property that never met the customer's criteria. - `greenfinch_update_lead_status` (writes; draws a daily limit; needs `pipeline:transitions_full`): Moves a deal to attempted_contact, active_opportunity, won or lost, with a written note of what happened and how the agent knows. - `greenfinch_get_property_activity`: Returns your organization's own activity history on one property (pipeline status changes, deal-value updates, notes, owner assignments), newest first. - `greenfinch_get_pipeline_analytics`: Returns the pipeline dashboard's summary numbers: how many deals are in each stage and their dollar totals, plus close rate and average time-to-close for deals that closed recently. - `greenfinch_get_loss_reasons`: Lists the reason codes this org can use when marking a deal lost or disqualifying it: its own custom codes plus the standard ones, each saying which of the two it applies to. - `greenfinch_get_transition_cap`: Shows how many agent pipeline changes (qualify, disqualify and the other stage moves, all counted together) are left today for the org and for this connection, and why the limit is what it is. - `greenfinch_get_deal_history`: Lists every pipeline stage change for one property, newest first: the stage it moved from and to, who made the change, when, how long the deal sat in the previous stage, any note an agent left, and the reason code of a lost or disqualified deal. - `greenfinch_sync_customer_status` (writes): Tells Greenfinch which properties are your organization's active or churned customer accounts, up to 500 per call. - `greenfinch_create_follow_up` (writes; needs a per-user connection): Sets a follow-up — a dated task on a property, with an owner, a type (follow_up, call, email, meeting, other) and an optional note. - `greenfinch_list_follow_ups`: Reads back the follow-ups (tasks) your organization has set — everything still pending, one property's, or one person's — soonest due first, each flagged when it is past due. - `greenfinch_complete_follow_up` (writes): Closes out a follow-up: completed, or cancelled when it is no longer needed. - `greenfinch_reschedule_follow_up` (writes): Moves an open follow-up to a new due date, and optionally replaces its note — the same reschedule the property page offers. - `greenfinch_set_deal_value` (writes): Sets a deal's dollar value at whatever stage it is in, without moving the stage. - `greenfinch_assign_owner` (writes): Makes a person the owner of a deal: yourself, or a teammate by their user id. ### Lead bundles - `greenfinch_get_lead_bundle` (writes; can cost credits; draws a daily limit): Returns the complete CRM-ready record for one property you have unlocked: property details, attached organizations and contacts, pipeline status, notes and actions. - `greenfinch_get_lead_bundles` (writes; can cost credits; draws a daily limit): Delivers CRM lead bundles for every property in a saved property list, one page at a time. ### Workspace and account - `greenfinch_describe_workspace`: Describes your workspace in one call: the counties you can see, the property categories in your visible data (with counts), every pipeline status, the credit price sheet, your lead-delivery allowance, limits, and step-by-step recipes for common workflows. - `greenfinch_get_activity`: Shows what agents (and optionally people) did in your organization recently and what it cost: property activity entries plus credit debits and refunds, newest first. - `greenfinch_get_emitted_events`: Lists the outbound events your organization has sent to a connected CRM or automation, newest first, with delivery status: lead events (for example after a lead is qualified) and, once signals are switched on, change events (signal.ownership_transfer, signal.sale, signal.manager_change, signal.contact_departure, signal.contact_title_change, signal.contact_employer_change, and signal.withdrawn when a change already sent is retracted), each naming its subject. - `greenfinch_get_quota_state`: Shows how your organization's next lead-bundle delivery would be billed: from the free monthly delivery allowance, or with credits once that allowance is used up. - `greenfinch_get_credit_balance`: Returns your organization's current spendable credit balance: the total plus how much sits in each pool (plan-included, purchased, trial, enterprise). - `greenfinch_get_entitlement_summary`: Explains which geography this connection can actually see and why: what the organization is licensed for, which area an admin has switched on, and the counties that are really visible after any personal territory is applied. - `greenfinch_get_reveal_history`: Lists the properties and contacts your organization has revealed over a recent window, newest first, with the credits charged for each. - `greenfinch_get_usage_breakdown`: Shows how many credits your organization has used in the current billing period, broken down by type of action. - `greenfinch_get_price_sheet`: Returns Greenfinch's full credit price list: how many credits each paid action costs. - `greenfinch_get_team`: Lists your organization's team members with their role, join date, and how many counties their personal territory covers. - `greenfinch_get_company_profile`: Reads the company profile Greenfinch keeps for your organization: description, brand values, differentiators, services, industry, size, headquarters and property focus. - `greenfinch_update_company_profile` (writes; needs a per-user connection): Updates your organization's company profile. ### Support and requests - `greenfinch_request_coverage` (writes): Asks the Greenfinch team to add data coverage for a county, identified by its 5-digit county code. - `greenfinch_request_refund` (writes): Requests an automatic credit refund for one of your organization's own earlier paid actions that failed or gave bad results. - `greenfinch_request_support` (writes): Sends a support request to the Greenfinch team for something the tools cannot do: a missing capability, data that looks wrong, or a billing or access question. - `greenfinch_get_support_requests`: Lists the support requests your organization has already filed, newest first. - `greenfinch_flag_entity` (writes): Reports that a property or contact record looks wrong, sending it to the queue Greenfinch staff work through to correct data, and automatically refunding every credit this organization spent on that record — research it commissioned, an unlock or reveal it paid for, an email lookup, a draft. - `greenfinch_get_flag_status`: Checks what became of the data-quality reports this organization filed: whether Greenfinch staff have acted on each one yet, plus what the automatic refund of what the organization spent did. - `greenfinch_submit_contact` (writes; needs a per-user connection): Tells Greenfinch about a person connected to a property — their name, an email address or LinkedIn URL, and their relationship to the building. - `greenfinch_get_submission_status` (needs a per-user connection): Checks what became of the contacts you submitted: each submission's outcome, its reward, and what the check of the LinkedIn URL it carried found. ## 7. Rate limits and refusal codes - Requests per minute, per key, one counter across every REST endpoint and the MCP endpoint, in fixed 60-second windows: Team 120, Enterprise 300. - Enterprise organizations also have a daily request ceiling across all keys and assistant connections together: 50,000 per UTC day by default. It resets at midnight UTC. - Before any key is checked, one IP address may send 1,000 requests per minute. - Daily data limits (contact reveals, lead deliveries, agent pipeline changes) and the Enterprise records allowance are set per organization and per key. `GET /allowance` and the app's Limits and usage page show the numbers in force for your organization. How to pace: - Successful responses carry no rate-limit headers. They carry only `x-request-id` (An identifier for the request. Quote it when you contact support.). You learn a limit when a request is refused, so read the refusal code and follow the action given for it. - A request-rate refusal (`rate_limited`, `DAILY_REQUEST_LIMIT_EXCEEDED`) carries: - `Retry-After`: Seconds to wait before retrying. The same number is in `meta.retryAfterSeconds`. - `X-RateLimit-Limit`: The per-minute limit that was reached. - `X-RateLimit-Remaining`: Always `0` on a refusal. - `X-RateLimit-Reset`: When the window ends, in Unix seconds. - Every other `429` carries no `X-RateLimit-*` headers, and whether it carries `Retry-After` depends on the endpoint. `GET /leads/{id}` sends it for every limit, counting down to when the limit lifts. The search, read and unlock endpoints send it for the daily limits (`DAILY_CAP_EXCEEDED`, `CREDENTIAL_DAILY_CAP_EXCEEDED`, `ACCOUNT_DAILY_CAP_EXCEEDED`) but not for `RECORDS_ALLOWANCE_EXCEEDED`, and for `RATE_LIMITED` only when the refusal names a reset time. `GET /properties/{id}` never sends it. Every limit refusal says when it lifts in `meta.limit`: `resets_at` for a daily limit, `period_ends_at` for the records allowance. - Every limit refusal carries a `limit` object in `meta` naming the limit, its `level` (1 request rate, 2 daily data, 3 records allowance), its value, what was used and when it lifts (`resets_at`, or `period_ends_at` for the records allowance). - On `429` or `503`, wait for `Retry-After`, or until the time in `meta.limit`, then retry with backoff. Do not retry a `403` or `402`; tell the user what the code says. HTTP statuses: - `400`: The request is malformed: bad JSON, a missing or invalid parameter. Fix the request. The message names the field. - `401`: No key, a malformed key, or a revoked key. Check the Authorization header; create a new key if it was revoked. - `402`: Not enough credits for a metered action. Top up or wait for the next allowance, then retry. - `403`: The key lacks the scope, the plan lacks the feature, the account is suspended or closed, or the item is outside your territory. Read the code; most need an admin or a plan change, not a retry. - `404`: The item does not exist or is not visible to your organization. Check the ID. - `409`: A conflict: a limit on keys or endpoints, a merged property, or a change during the request. Read the code; `CHANGED_IN_FLIGHT` can be retried. - `422`: The request is well-formed but a value is not allowed. Fix the value. - `429`: A rate limit, a daily limit or the records allowance was reached. Wait for Retry-After, or for the reset time in the limit object. - `503`: A safety check or the rate limiter was briefly unavailable. Retry after Retry-After, with backoff. - `500`: An unexpected server error. Retry with backoff; contact support if it persists. Refusal codes (76). On REST the code is in `meta.code`, or top-level `code` for errors raised before the endpoint runs. On MCP a tool refusal puts it in `result.structuredContent.code`. "MCP" means the code comes only from a connector tool. ### Access and account - `SCOPE_NOT_GRANTED` (403): The key was not given the scope this call needs. Create a key with it. - `UPGRADE_REQUIRED` (403): Your plan does not include this feature; `feature` and `minimumTier` say which. Upgrade, or contact sales for Enterprise. - `ORG_SUSPENDED` (403): The organization is suspended. Contact support. - `ORG_SUSPENSION_UNAVAILABLE` (503): Account status could not be confirmed. Retry after Retry-After. - `ACCOUNT_CLOSED` (403): The account is closed. - `ACCOUNT_CLOSURE_CHECK_UNAVAILABLE` (503): Account status could not be confirmed. Retry shortly. - `CREDENTIAL_LIMIT_REACHED` (409): The organization already has 10 active keys. Revoke one first. - `rate_limited` (429): This key's per-minute request rate is used. Wait `Retry-After` seconds. Lower case, and different from `RATE_LIMITED`. - `rate_limit_unavailable` (503): The rate limiter is unavailable. Retry with backoff. - `INVALID_OAUTH_TOKEN` (401): A per-user connection's sign-in token is invalid or expired. Reconnect. - `NOT_A_MEMBER` (403): The signed-in person is not a member of the organization. - `MEMBER_CONNECT_DISABLED` (403): The organization only lets admins connect assistants. - `GRANT_REVOKED` (403): This connection was revoked. An admin must restore access. - `NO_ACTIVE_SEAT` (MCP): The member has no active seat. - `ROLE_NOT_PERMITTED` (MCP): The member's role is read-only; the tool writes or spends. - `ACTING_USER_REQUIRED` (MCP / 403): This action must be attributed to a person. Use a per-user connection. - `ACTING_USER_NOT_SUPPORTED` (403): The search, read and unlock REST endpoints accept organization API keys only; a signed-in person's connection uses MCP. - `TERMS_ACCEPTANCE_REQUIRED` (403): A person connecting an assistant has not accepted Greenfinch's current Terms. They sign in to the Greenfinch app, accept them, and connect again. - `ORG_CONTEXT_REQUIRED` (403): A person's sign-in does not name one Greenfinch organization, either because they belong to none or because they must choose one. They pick the organization when connecting. - `IDENTITY_UNRESOLVED` (MCP / 503): The member's account could not be resolved. Retry shortly. ### Credits and limits - `INSUFFICIENT_CREDITS` (402 / MCP): Not enough credits; `required` and `available` give the numbers. The same code covers a member's personal monthly credit limit. - `CREDITS_DORMANT` (402): The organization's credits cannot be spent on its current plan. - `USER_CREDIT_LIMIT_EXCEEDED` (402): A per-member credit limit was reached. - `DAILY_REQUEST_LIMIT_EXCEEDED` (429): The organization's daily request ceiling is used (level 1). Resume after the reset time in the limit object. - `ACCOUNT_DAILY_CAP_EXCEEDED` (429 / MCP): A Greenfinch account maximum for this kind of data is used (level 2). Your own admins cannot raise it. When the refusal carries `derived` `true` the number was worked out from your records allowance rather than typed in by anyone, and a larger allowance raises it; otherwise contact your Greenfinch contact. - `DAILY_CAP_EXCEEDED` (429 / MCP): The organization's daily limit for this action is used. Wait for `resetsAt`, or an admin raises it. - `CREDENTIAL_DAILY_CAP_EXCEEDED` (429 / MCP): This key's or connection's daily limit is used. - `RECORDS_ALLOWANCE_EXCEEDED` (429 / MCP): The request would take more records than remain in the records allowance (level 3). Looking in the app is unaffected. - `RECORDS_ALLOWANCE_NOT_SET` (409 / MCP): The organization's order has no records allowance recorded yet, so nothing can be taken. Waiting does not fix it and there is no `Retry-After`: contact Greenfinch. Changed 2026-09-16 from 429 to 409 on `GET /leads/{id}` and `GET /properties/{id}`; no retry fixes it. - `COST_CONFIRMATION_REQUIRED` (MCP): The call would cost more than `maxCredits`. Re-run with a ceiling at or above `projectedCredits`. - `ACTION_IN_PROGRESS` (409 / MCP): Another request is already unlocking or revealing the same item. Nothing was charged. Retry after Retry-After (1 second) or `retryAfterSeconds`; if the first finished, the repeat is free. - `PAYMENT_FAILED` (402 / MCP): The subscription payment failed, so unlocks and reveals cannot spend credits. Nothing was charged; an admin updates the payment method on the Billing page. - `CREDIT_ACTION_IN_PROGRESS` (MCP): The same paid action is already running. Retry after `retryAfterSeconds`. - `RATE_LIMITED` (429 / MCP): Too many requests of one kind (support, coverage or research requests). Slow down. Upper case, and different from the per-minute `rate_limited`. - `SUBSCRIPTION_PAYMENT_FAILED` (MCP): Billing needs attention before this paid action can run. - `BILLING_NOT_PROVISIONED` (MCP): Billing is not set up for this organization yet. Contact support. ### Coverage and visibility - `OUT_OF_TERRITORY` (403 / MCP): The item exists but is outside your service area (reason `subscription`) or the member's assigned territory (reason `territory`). The message names the county. - `NO_VISIBLE_TERRITORY` (403 / MCP): Nothing is visible to this caller at all; `cause` says why. Usually an admin setting. - `NOT_ENTITLED` (403 on `GET /leads/{id}` / MCP): The property is not unlocked or covered. Unlock it first. - `FEATURE_GATED` (403 / MCP): Your plan does not include this feature (for example lists or the pipeline). - `NOT_FOUND` (404 / MCP): No such item visible to you. - `PROPERTY_RETIRED` (409 / MCP): The property was merged after a county data refresh. Use `successorPropertyId` from now on. - `PROPERTY_NOT_AVAILABLE` (404 / MCP): A property in the request is missing or outside your territory. - `CONTACT_NOT_AVAILABLE` (404 / MCP): A contact in the request is not available to your organization. - `LIST_NOT_FOUND` (MCP): No such list, or it is another member's private list. - `COMPLIANCE_CHECK_UNAVAILABLE` (503 on `GET /leads/{id}` / MCP): A safety check could not run. Retry shortly. ### Pipeline and write-back - `NO_PIPELINE_RECORD` (404): Write-back only updates leads that were in your pipeline. - `CRM_MODE_NOT_EXTERNAL` (409): Your organization manages stages in Greenfinch. An admin can switch CRM mode to external. - `CHANGED_IN_FLIGHT` (409): The lead changed during the request. Retry. - `STAGE_CAPPED` (MCP): Your external CRM owns this stage; record it there. - `MCP_QUALIFICATION_DISABLED` (MCP): An admin switched off agent pipeline changes. - `SCOPE_REQUIRED` (MCP): `greenfinch_update_lead_status` needs `pipeline:transitions_full` on the connection. - `DEAL_VALUE_REQUIRED` (MCP): Won (and qualify) need a deal value above $1. - `INVALID_LOSS_REASON_CODE` (MCP): Use a code from `greenfinch_get_loss_reasons`. - `SAME_STATUS` (MCP): The lead is already in that stage. - `ALREADY_DISQUALIFIED` (MCP): The lead is already disqualified. - `CLOSED_STAGE` (MCP): Won or lost leads cannot be disqualified. ### Webhooks - `WEBHOOK_ENDPOINT_LIMIT_REACHED` (409): The organization already has its maximum number of active webhook endpoints. Remove one you no longer use, or contact support. - `WEBHOOK_ENDPOINT_TOTAL_LIMIT_REACHED` (409): The organization has reached its total webhook-endpoint limit, which counts removed endpoints too. Contact support. ### Research, lists and search - `NOT_RESEARCHED` (409 / MCP): The property has no research to unlock (409 on REST). Start research first. - `PROPERTY_UNLOCK_REQUIRED` (MCP): Unlock the property before this contact action. - `CONTACT_REVEAL_REQUIRED` (MCP): Reveal the contact before this action. - `STRATEGY_REQUIRED` (MCP): Run deep research on the contact before drafting an email. - `ALREADY_RESEARCHING` (MCP): Research is already running for this item. - `BATCH_ALREADY_RUNNING` (MCP): A research batch is already running; wait or cancel it. - `RESEARCH_LAUNCH_BUSY` (MCP): Another research launch for your organization is being recorded. Nothing was charged; retry in a few seconds. - `BATCH_TOO_LARGE` (MCP): Too many properties in one research batch. Split it. - `EMPTY_SELECTION` (MCP): The selection has no eligible properties. - `WRONG_LIST_TYPE` (MCP): The list holds the other kind of item (properties versus contacts). - `NAME_IN_USE` (MCP): A list with that name already exists. - `MISSING_QUERY_BOUNDS_OR_COUNTY` (400 / MCP): A search needs free text, a location filter or map bounds. - `AMBIGUOUS_SEARCH_MODE` (400 / MCP): Send free text or filters/bounds, not both. - `INVALID_CURSOR` (400 / MCP): The paging cursor is malformed, or its sort value has the wrong type. Start again from the first page. - `CURSOR_SORT_MISMATCH` (400 / MCP): A cursor must be used with the sort it came from; a cursor in an older format also gets this. Start again from the first page. - `OFFSET_REQUIRES_SORT` (MCP): Paging a saved search with offset needs an explicit sort. - `TEMPORARILY_UNAVAILABLE` (MCP): A dependency is briefly unavailable. Retry shortly. ## 8. Record types and delivered fields Search and detail endpoints return the shapes shown in section 5. The full record a program takes is the lead bundle: one property with its firms, its people, your pipeline state and your notes, nested in one JSON object. It is delivered by `GET /leads/{id}` (scope `leads:read_single`), by `greenfinch_get_lead_bundle`, and in every webhook delivery. It reaches you through: - REST API: One record at a time over HTTPS, with an organization API key. - AI assistant: The same records through a connected AI assistant. Fields marked optional may be missing or null; the reason follows. Your administrators can hide some fields from API keys, and a contact's details appear only once it is revealed. ### lead_bundles (215 fields) - `bundle_version` (`1`): The version of the lead-bundle shape this record follows. Fields can be added, and a field that never carried a value can be removed, without changing it. A populated field is never removed within a version. - `greenfinch_property_id` (`string`): Greenfinch's identifier for the property. It is the only property identifier Greenfinch publishes, it is stable, and it is what every other row and every de-duplication check keys on. - `generated_at` (`string`): When Greenfinch assembled this record, as an ISO-8601 timestamp. - `delivered_to_org` (`string`): Your organization, echoed back so a receiving system can tell deliveries apart. - `partial` (`boolean`): Whether this is a reduced record for a property you have qualified but never revealed. Only the outbound feed to your own systems ever sends one; asking for a record directly is refused instead, and a reduced record carries no people at all. - `partial_reason` (`"not_revealed" | null`): Why the record is reduced, when it is. Optional: The record is complete, which is the ordinary case. - `property.address` (`string`): The property's street address, validated where validation succeeded and otherwise the county's own line. Always a string, though it can be an empty one where no source carries an address at all. - `property.city` (`string | null`): The city. Optional: The county's record carries no city for this parcel. - `property.state` (`string | null`): The two-letter state code. Optional: The county's record carries no state for this parcel. - `property.zip` (`string | null`): The postal code. Optional: The county's record carries no postal code for this parcel. - `property.county` (`string | null`): The county the property sits in. Optional: The county name was not resolved for this parcel. - `property.lat` (`number | null`): Latitude of the parcel's centre point. Optional: The parcel has no mapped boundary to take a centre point from. - `property.lon` (`number | null`): Longitude of the parcel's centre point. Optional: The parcel has no mapped boundary to take a centre point from. - `property.common_name` (`string | null`): The name the property is known by — a building, park or campus name rather than its address. Optional: Greenfinch's research has not produced it for this property. A property whose research has never run carries none of the researched fields. - `property.common_name_confidence` (`number | null`): How confident Greenfinch is in that name, from 0 to 1. At or below 0.4 the name is an honest address-style stand-in rather than a name anyone uses, so do not present it as one. Optional: No name was produced, so there is nothing to score. - `property.lot_sqft` (`number | null`): The lot's area in square feet. Optional: No source Greenfinch holds states a lot area for this parcel. - `property.building_sqft` (`number | null`): The building's area in square feet. A county figure at or under 100 square feet is a placeholder for a building nobody measured, not a size, so it is never delivered (the floor is inclusive: 100 itself is a placeholder). Optional: No source Greenfinch holds states a building area — common on land with no building on it, and on a building whose county record carries only the placeholder figure. - `property.building_sqft_is_planned` (`boolean`): Whether that building area describes something planned or approved rather than something standing today. - `property.year_built` (`number | null`): The year the building was built. Where a county lists every building on a site, it is the year of the building with the most floor area, or, when that building's record gives no year, of the largest building that does. Optional: No source Greenfinch holds states a year built. - `property.year_built_detail` (`object | null`): How the year built value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The year_built value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the year_built value) — the value itself is still delivered. - `property.year_built_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the year_built value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.year_built_detail is empty: The year_built value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the year_built value) — the value itself is still delivered. - `property.year_built_detail.oldest` (`number | null`): The earliest year built among the site's buildings. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. One building's year says nothing about the others. It is also empty whenever property.year_built_detail is. - `property.year_built_detail.newest` (`number | null`): The latest year built among the site's buildings. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. One building's year says nothing about the others. It is also empty whenever property.year_built_detail is. - `property.year_built_detail.primary_building_basis` (`"area" | "record_order" | "largest_stated" | null`): Which building's year is the year_built value: area for the building with the most floor area; record_order for the county's first-listed building, when floor areas tie or are unknown; largest_stated for the largest building whose record gives a year, when the main building's record gives none. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.year_built_detail is. - `property.year_built_detail.buildings` (`number | null`): How many of the site's buildings state a year built. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.year_built_detail is. - `property.year_built_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.year_built_detail is. - `property.num_floors` (`number | null`): How many floors the building has above ground, as a whole number. Where a county lists every building on a site, it is the tallest building's floor count, rounded up to a whole floor. Optional: No source Greenfinch holds states a floor count. - `property.num_floors_detail` (`object | null`): How the floor count value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The num_floors value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the num_floors value) — the value itself is still delivered. - `property.num_floors_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the num_floors value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.num_floors_detail is empty: The num_floors value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the num_floors value) — the value itself is still delivered. - `property.num_floors_detail.max` (`number`): The tallest building's floor count exactly as the county records it, which can be a fraction (1.5 storeys); num_floors is this figure rounded up to a whole floor. Optional: Only when property.num_floors_detail is empty: The num_floors value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the num_floors value) — the value itself is still delivered. - `property.num_floors_detail.area_weighted_mean` (`number | null`): The average floor count across the site's buildings, weighted by each building's floor area. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.num_floors_detail is. - `property.num_floors_detail.primary_building` (`number | null`): The floor count of the site's main building: the one with the most floor area, or, when floor areas tie or are unknown, the building the county lists first (primary_building_basis says which). Optional: The main building's record gives no floor count, or the detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.num_floors_detail is. - `property.num_floors_detail.primary_building_basis` (`"area" | "record_order" | null`): How the main building was chosen: area when it has the most floor area; record_order when floor areas tie or are unknown and the county's first-listed building was taken. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.num_floors_detail is. - `property.num_floors_detail.buildings` (`number | null`): How many of the site's buildings state a floor count. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.num_floors_detail is. - `property.num_floors_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.num_floors_detail is. - `property.effective_year_built` (`number | null`): The year the county treats the building as dating from once renovations are taken into account — later than the year built when the building has been substantially updated. On a site with several buildings it follows the same rule as the year built: the building with the most floor area, or, when that building's record gives no effective year, the largest building that does. Optional: It is filled only where a county publishes it, and most counties do not carry it today; where one does, it is often filled for only some of its buildings. Where a county does publish it, its records say nothing about it for this property. - `property.effective_year_built_detail` (`object | null`): How the effective year built value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The effective_year_built value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the effective_year_built value) — the value itself is still delivered. - `property.effective_year_built_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the effective_year_built value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.effective_year_built_detail is empty: The effective_year_built value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the effective_year_built value) — the value itself is still delivered. - `property.effective_year_built_detail.oldest` (`number | null`): The earliest effective year built among the site's buildings. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. One building's year says nothing about the others. It is also empty whenever property.effective_year_built_detail is. - `property.effective_year_built_detail.newest` (`number | null`): The latest effective year built among the site's buildings. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. One building's year says nothing about the others. It is also empty whenever property.effective_year_built_detail is. - `property.effective_year_built_detail.primary_building_basis` (`"area" | "record_order" | "largest_stated" | null`): Which building's year is the effective_year_built value: area for the building with the most floor area; record_order for the county's first-listed building, when floor areas tie or are unknown; largest_stated for the largest building whose record gives a year, when the main building's record gives none. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.effective_year_built_detail is. - `property.effective_year_built_detail.buildings` (`number | null`): How many of the site's buildings state a effective year built. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.effective_year_built_detail is. - `property.effective_year_built_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.effective_year_built_detail is. - `property.heating_type` (`string | null`): The kind of heating system the building uses: Forced Air Furnace, Heat Pump, Hot Water or Steam Boiler, Electric Resistance, Rooftop Package Unit, Radiant, Space or Wall Heater, None or Other. On a site with several buildings it is the value covering the most building floor area. Optional: It is filled only where a county publishes it, and most counties do not carry it today; where one does, it is often filled for only some of its buildings. Where a county does publish it, its records say nothing about it for this property. - `property.heating_type_detail` (`object | null`): How the heating type value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The heating_type value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the heating_type value) — the value itself is still delivered. - `property.heating_type_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the heating_type value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.heating_type_detail is empty: The heating_type value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the heating_type value) — the value itself is still delivered. - `property.heating_type_detail.buildings` (`number | null`): How many of the site's buildings state a heating type. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.heating_type_detail is. - `property.heating_type_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.heating_type_detail is. - `property.heating_type_detail.area_share` (`number | null`): The share of the site's known building floor area, from 0 to 1, in buildings that state a heating type. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.heating_type_detail is. - `property.heating_type_detail.values[].value` (`string`): One heating type found on the site. The list is ordered by share of floor area, then by number of buildings, and its first entry is always the heating_type value. Optional: Only when property.heating_type_detail is empty: The heating_type value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the heating_type value) — the value itself is still delivered. - `property.heating_type_detail.values[].buildings` (`number | null`): How many buildings have this value. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.heating_type_detail is. - `property.heating_type_detail.values[].area_share` (`number | null`): This value's share of the site's known building floor area, from 0 to 1. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.heating_type_detail is. - `property.cooling_type` (`string | null`): The kind of cooling system the building uses: Central Air, Rooftop Package Unit, Chilled Water, Heat Pump, Packaged Terminal, Evaporative, Window or Wall Unit, None or Other. On a site with several buildings it is the value covering the most building floor area. Optional: It is filled only where a county publishes it, and most counties do not carry it today; where one does, it is often filled for only some of its buildings. Where a county does publish it, its records say nothing about it for this property. - `property.cooling_type_detail` (`object | null`): How the cooling type value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The cooling_type value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the cooling_type value) — the value itself is still delivered. - `property.cooling_type_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the cooling_type value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.cooling_type_detail is empty: The cooling_type value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the cooling_type value) — the value itself is still delivered. - `property.cooling_type_detail.buildings` (`number | null`): How many of the site's buildings state a cooling type. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.cooling_type_detail is. - `property.cooling_type_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.cooling_type_detail is. - `property.cooling_type_detail.area_share` (`number | null`): The share of the site's known building floor area, from 0 to 1, in buildings that state a cooling type. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.cooling_type_detail is. - `property.cooling_type_detail.values[].value` (`string`): One cooling type found on the site. The list is ordered by share of floor area, then by number of buildings, and its first entry is always the cooling_type value. Optional: Only when property.cooling_type_detail is empty: The cooling_type value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the cooling_type value) — the value itself is still delivered. - `property.cooling_type_detail.values[].buildings` (`number | null`): How many buildings have this value. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.cooling_type_detail is. - `property.cooling_type_detail.values[].area_share` (`number | null`): This value's share of the site's known building floor area, from 0 to 1. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.cooling_type_detail is. - `property.primary_construction_type` (`string | null`): What the building's structure is mainly made of: Wood Frame, Masonry, Concrete, Steel Frame, Pre-Engineered Metal, Tilt-Up, Mixed or Other. On a site with several buildings it is the value covering the most building floor area. Optional: It is filled only where a county publishes it, and most counties do not carry it today; where one does, it is often filled for only some of its buildings. Where a county does publish it, its records say nothing about it for this property. - `property.primary_construction_type_detail` (`object | null`): How the construction type value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The primary_construction_type value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the primary_construction_type value) — the value itself is still delivered. - `property.primary_construction_type_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the primary_construction_type value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.primary_construction_type_detail is empty: The primary_construction_type value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the primary_construction_type value) — the value itself is still delivered. - `property.primary_construction_type_detail.buildings` (`number | null`): How many of the site's buildings state a construction type. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.primary_construction_type_detail is. - `property.primary_construction_type_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.primary_construction_type_detail is. - `property.primary_construction_type_detail.area_share` (`number | null`): The share of the site's known building floor area, from 0 to 1, in buildings that state a construction type. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.primary_construction_type_detail is. - `property.primary_construction_type_detail.values[].value` (`string`): One construction type found on the site. The list is ordered by share of floor area, then by number of buildings, and its first entry is always the primary_construction_type value. Optional: Only when property.primary_construction_type_detail is empty: The primary_construction_type value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the primary_construction_type value) — the value itself is still delivered. - `property.primary_construction_type_detail.values[].buildings` (`number | null`): How many buildings have this value. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.primary_construction_type_detail is. - `property.primary_construction_type_detail.values[].area_share` (`number | null`): This value's share of the site's known building floor area, from 0 to 1. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.primary_construction_type_detail is. - `property.exterior_wall_material` (`string | null`): What the building's outside walls are faced with: Brick, Stucco or EIFS, Vinyl or Aluminum Siding, Wood Siding, Concrete or Block, Metal Panel, Glass Curtain Wall, Stone or Other. On a site with several buildings it is the value covering the most building floor area. Optional: It is filled only where a county publishes it, and most counties do not carry it today; where one does, it is often filled for only some of its buildings. Where a county does publish it, its records say nothing about it for this property. - `property.exterior_wall_material_detail` (`object | null`): How the exterior wall material value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The exterior_wall_material value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the exterior_wall_material value) — the value itself is still delivered. - `property.exterior_wall_material_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the exterior_wall_material value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.exterior_wall_material_detail is empty: The exterior_wall_material value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the exterior_wall_material value) — the value itself is still delivered. - `property.exterior_wall_material_detail.buildings` (`number | null`): How many of the site's buildings state a exterior wall material. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.exterior_wall_material_detail is. - `property.exterior_wall_material_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.exterior_wall_material_detail is. - `property.exterior_wall_material_detail.area_share` (`number | null`): The share of the site's known building floor area, from 0 to 1, in buildings that state a exterior wall material. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.exterior_wall_material_detail is. - `property.exterior_wall_material_detail.values[].value` (`string`): One exterior wall material found on the site. The list is ordered by share of floor area, then by number of buildings, and its first entry is always the exterior_wall_material value. Optional: Only when property.exterior_wall_material_detail is empty: The exterior_wall_material value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the exterior_wall_material value) — the value itself is still delivered. - `property.exterior_wall_material_detail.values[].buildings` (`number | null`): How many buildings have this value. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.exterior_wall_material_detail is. - `property.exterior_wall_material_detail.values[].area_share` (`number | null`): This value's share of the site's known building floor area, from 0 to 1. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.exterior_wall_material_detail is. - `property.foundation_type` (`string | null`): What the building stands on: Slab on Grade, Crawl Space, Full Basement, Partial Basement, Pier and Beam, Piling or Other. On a site with several buildings it is the value covering the most building floor area. Optional: It is filled only where a county publishes it, and most counties do not carry it today; where one does, it is often filled for only some of its buildings. Where a county does publish it, its records say nothing about it for this property. - `property.foundation_type_detail` (`object | null`): How the foundation type value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The foundation_type value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the foundation_type value) — the value itself is still delivered. - `property.foundation_type_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the foundation_type value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.foundation_type_detail is empty: The foundation_type value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the foundation_type value) — the value itself is still delivered. - `property.foundation_type_detail.buildings` (`number | null`): How many of the site's buildings state a foundation type. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.foundation_type_detail is. - `property.foundation_type_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.foundation_type_detail is. - `property.foundation_type_detail.area_share` (`number | null`): The share of the site's known building floor area, from 0 to 1, in buildings that state a foundation type. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.foundation_type_detail is. - `property.foundation_type_detail.values[].value` (`string`): One foundation type found on the site. The list is ordered by share of floor area, then by number of buildings, and its first entry is always the foundation_type value. Optional: Only when property.foundation_type_detail is empty: The foundation_type value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the foundation_type value) — the value itself is still delivered. - `property.foundation_type_detail.values[].buildings` (`number | null`): How many buildings have this value. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.foundation_type_detail is. - `property.foundation_type_detail.values[].area_share` (`number | null`): This value's share of the site's known building floor area, from 0 to 1. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.foundation_type_detail is. - `property.roof_cover` (`string | null`): What the roof's surface is made of: Asphalt Shingle, Metal, Built-Up or Modified Bitumen, Single-Ply Membrane, Tile, Wood Shake or Shingle, Slate or Other. On a site with several buildings it is the value covering the most building floor area. Optional: It is filled only where a county publishes it, and most counties do not carry it today; where one does, it is often filled for only some of its buildings. Where a county does publish it, its records say nothing about it for this property. - `property.roof_cover_detail` (`object | null`): How the roof covering value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The roof_cover value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the roof_cover value) — the value itself is still delivered. - `property.roof_cover_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the roof_cover value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.roof_cover_detail is empty: The roof_cover value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the roof_cover value) — the value itself is still delivered. - `property.roof_cover_detail.buildings` (`number | null`): How many of the site's buildings state a roof covering. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.roof_cover_detail is. - `property.roof_cover_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.roof_cover_detail is. - `property.roof_cover_detail.area_share` (`number | null`): The share of the site's known building floor area, from 0 to 1, in buildings that state a roof covering. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.roof_cover_detail is. - `property.roof_cover_detail.values[].value` (`string`): One roof covering found on the site. The list is ordered by share of floor area, then by number of buildings, and its first entry is always the roof_cover value. Optional: Only when property.roof_cover_detail is empty: The roof_cover value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the roof_cover value) — the value itself is still delivered. - `property.roof_cover_detail.values[].buildings` (`number | null`): How many buildings have this value. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.roof_cover_detail is. - `property.roof_cover_detail.values[].area_share` (`number | null`): This value's share of the site's known building floor area, from 0 to 1. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.roof_cover_detail is. - `property.roof_shape` (`string | null`): The roof's shape, as one value from a fixed list of roof shapes — among them Gable, Hip, Shed, Mixed and Other. On a site with several buildings it is the value covering the most building floor area. Optional: It is filled only where a county publishes it, and most counties do not carry it today; where one does, it is often filled for only some of its buildings. Where a county does publish it, its records say nothing about it for this property. - `property.roof_shape_detail` (`object | null`): How the roof shape value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The roof_shape value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the roof_shape value) — the value itself is still delivered. - `property.roof_shape_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the roof_shape value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.roof_shape_detail is empty: The roof_shape value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the roof_shape value) — the value itself is still delivered. - `property.roof_shape_detail.buildings` (`number | null`): How many of the site's buildings state a roof shape. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.roof_shape_detail is. - `property.roof_shape_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.roof_shape_detail is. - `property.roof_shape_detail.area_share` (`number | null`): The share of the site's known building floor area, from 0 to 1, in buildings that state a roof shape. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.roof_shape_detail is. - `property.roof_shape_detail.values[].value` (`string`): One roof shape found on the site. The list is ordered by share of floor area, then by number of buildings, and its first entry is always the roof_shape value. Optional: Only when property.roof_shape_detail is empty: The roof_shape value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the roof_shape value) — the value itself is still delivered. - `property.roof_shape_detail.values[].buildings` (`number | null`): How many buildings have this value. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.roof_shape_detail is. - `property.roof_shape_detail.values[].area_share` (`number | null`): This value's share of the site's known building floor area, from 0 to 1. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.roof_shape_detail is. - `property.quality_grade` (`string | null`): The construction-quality grade the county assessor recorded in order to value the building for tax: Excellent, Very Good, Good, Average, Fair, Poor or Other, on that county's own scale. It is a tax-valuation tier, not an inspection and not a statement about the building's condition today — nobody visited the property, and the grade may be years old — so use it as a rough tier for sizing work, never as a report of defects. On a site with several buildings it is the value covering the most building floor area. Optional: It is filled only where a county publishes it, and most counties do not carry it today; where one does, it is often filled for only some of its buildings. Where a county does publish it, its records say nothing about it for this property. - `property.quality_grade_detail` (`object | null`): How the quality grade value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The quality_grade value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the quality_grade value) — the value itself is still delivered. - `property.quality_grade_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the quality_grade value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.quality_grade_detail is empty: The quality_grade value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the quality_grade value) — the value itself is still delivered. - `property.quality_grade_detail.buildings` (`number | null`): How many of the site's buildings state a quality grade. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.quality_grade_detail is. - `property.quality_grade_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.quality_grade_detail is. - `property.quality_grade_detail.area_share` (`number | null`): The share of the site's known building floor area, from 0 to 1, in buildings that state a quality grade. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.quality_grade_detail is. - `property.quality_grade_detail.values[].value` (`string`): One quality grade found on the site. The list is ordered by share of floor area, then by number of buildings, and its first entry is always the quality_grade value. Optional: Only when property.quality_grade_detail is empty: The quality_grade value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the quality_grade value) — the value itself is still delivered. - `property.quality_grade_detail.values[].buildings` (`number | null`): How many buildings have this value. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.quality_grade_detail is. - `property.quality_grade_detail.values[].area_share` (`number | null`): This value's share of the site's known building floor area, from 0 to 1. Optional: The county gives no floor area for the site's buildings, or the detail comes from a single main-building record. It is also empty whenever property.quality_grade_detail is. - `property.has_pool` (`boolean | null`): Whether the property has a swimming pool or water feature: true when at least one building has one, false only when the county's records say there is none. Optional: It is filled only where a county publishes it, and most counties do not carry it today; where one does, it is often filled for only some of its buildings. Where a county does publish it, its records do not say either way for this property; a blank is never read as no. - `property.has_pool_detail` (`object | null`): How the a pool value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The has_pool value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the has_pool value) — the value itself is still delivered. - `property.has_pool_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the has_pool value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.has_pool_detail is empty: The has_pool value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the has_pool value) — the value itself is still delivered. - `property.has_pool_detail.count` (`number | null`): How many of the site's buildings have a pool. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.has_pool_detail is. - `property.has_pool_detail.buildings` (`number | null`): How many of the site's buildings say whether they have a pool at all, so "none of fourteen" can be told apart from "one said no and thirteen said nothing". Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.has_pool_detail is. - `property.has_pool_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.has_pool_detail is. - `property.has_sprinklers` (`boolean | null`): Whether the property has an automatic fire sprinkler system: true when at least one building has one, false only when the county's records say there is none. Optional: It is filled only where a county publishes it, and most counties do not carry it today; where one does, it is often filled for only some of its buildings. Where a county does publish it, its records do not say either way for this property; a blank is never read as no. - `property.has_sprinklers_detail` (`object | null`): How the sprinklers value breaks down across the site's buildings. Present whenever the value is, except as below. Optional: The has_sprinklers value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the has_sprinklers value) — the value itself is still delivered. - `property.has_sprinklers_detail.basis` (`"per_building" | "main_building_only"`): How to read this detail: per_building when Greenfinch combined the county's records of every building on the site; main_building_only when the has_sprinklers value comes from one main-building record, in which case the counts in this detail are empty. Optional: Only when property.has_sprinklers_detail is empty: The has_sprinklers value is empty, so there is nothing to break down; or, rarely, Greenfinch holds a per-building breakdown for this property that could not be read (it is missing, malformed, or disagrees with the has_sprinklers value) — the value itself is still delivered. - `property.has_sprinklers_detail.count` (`number | null`): How many of the site's buildings have sprinklers. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.has_sprinklers_detail is. - `property.has_sprinklers_detail.buildings` (`number | null`): How many of the site's buildings say whether they have sprinklers at all, so "none of fourteen" can be told apart from "one said no and thirteen said nothing". Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.has_sprinklers_detail is. - `property.has_sprinklers_detail.buildings_total` (`number | null`): How many buildings the county lists on the site. Optional: The detail comes from a single main-building record, so nothing is known about the site's other buildings. It is also empty whenever property.has_sprinklers_detail is. - `property.building_attributes_basis` (`"per_building" | "main_building_only" | null`): How this property's building details were put together: per_building when the county lists each building on the site and Greenfinch combined them; main_building_only when they come from one main-building record. Optional: The property carries none of the building details above: every one of them is empty. The footprint below does not count towards it. - `property.footprint_area` (`number | null`): The area the building covers on the ground, in square feet: the figure Greenfinch's property page shows. footprint_area_source says where it comes from. It has no detail object. Optional: Greenfinch holds no footprint for this property, or your plan does not include premium property data, in which case it is withheld with its source. - `property.footprint_area_source` (`"county_record" | "measured" | "estimate" | null`): Where the footprint comes from: measured when it is taken from the building's outline on a map; estimate when it is the building's floor area divided by its floor count; county_record when it is the county's recorded foundation area. No county's recorded foundation area feeds this figure yet, so county_record does not occur today; every property reads measured, estimate or empty. Optional: The footprint is empty, or withheld because your plan does not include premium property data. - `property.asset_category` (`string | null`): What kind of property it is at the top level — office, retail, industrial, multifamily and so on. Optional: Greenfinch's research has not produced it for this property. A property whose research has never run carries none of the researched fields. - `property.asset_subcategory` (`string | null`): The narrower kind within that category. Optional: The category was decided but no narrower kind applied. - `property.category_confidence` (`number | null`): How confident Greenfinch is in that classification, from 0 to 1. Optional: No classification was produced, so there is nothing to score. - `property.beneficial_owner` (`string | null`): Who really owns the property, once Greenfinch has looked through holding companies and trusts to the party that benefits. Optional: Research has not run, or it could not reach a conclusion it was willing to publish. - `property.beneficial_owner_type` (`string | null`): What kind of party that owner is — a private investor, an institution, a public body, an owner-occupier and so on. Optional: No beneficial owner was concluded, so there is nothing to type. - `property.beneficial_owner_confidence` (`number | null`): How confident Greenfinch is in that conclusion, from 0 to 1. Optional: No beneficial owner was concluded, so there is nothing to score. - `property.ownership_type` (`string | null`): How the property is held, in the terms a salesperson works in rather than a legal one. Optional: Research has not reached a view on how the property is held. - `property.management_company` (`string | null`): The firm that manages the property day to day, where that is not the owner. Optional: No managing firm was found, which is normal for owner-managed property. - `property.registered_owner` (`string | null`): The owner of record as the county states it, with the name normalised. It is the county's answer, not Greenfinch's conclusion about who really benefits. Optional: The county names no owner for this parcel. - `property.assessed_total_val` (`number | null`): The county's total assessed value for the parcel, in dollars. Optional: The county's record carries no total assessed value. - `property.last_sale_date` (`string | null`): When the property last sold, as the county's tax roll records the sale. Optional: The county records no sale for this parcel. - `property.last_sale_price` (`number | null`): What it sold for in that same sale, in dollars. Optional: The county records no sale, or the sale records no price — common on a sale between related parties and in states that do not disclose prices. - `property.last_transfer_date` (`string | null`): A later change of title the county records after the last sale — a sale, a move into a related company, an inheritance or a correction. Optional: The county records no transfer after the last sale, or only one in the same month as the sale. - `property.revenue_estimates` (`Record< string, { mid: number; low: number; high: number; tier: number } > | null`): What the property is likely worth in annual revenue to you, per service category, as a low, middle and high figure with a confidence tier. Optional: Too little is known about the property to estimate. - `property.property_website` (`string | null`): The property's own website, where it has one Greenfinch could confirm. Optional: No website was found, or one was found but could not be confirmed as this property's. - `property.ai_rationale` (`string | null`): Greenfinch's written account of the property — what it is, who is behind it, and why it was classified the way it was. The full text, not the shortened form the app shows before a property is revealed and not the shortened form a property search returns. Optional: Greenfinch's research has not produced it for this property. A property whose research has never run carries none of the researched fields. - `property.property_phone` (`string | null`): A phone number for the property itself, such as a leasing or management office line. Optional: No number was found for the property. - `property.property_phone_confidence` (`number | null`): How confident Greenfinch is in that number, from 0 to 1. Optional: No number was found, so there is nothing to score. - `property.last_enriched_at` (`string | null`): When Greenfinch's research last ran on this property, so you can tell fresh records from stale ones. Empty is the honest signal that research has never run. Optional: Research has never run on this property. - `property.deep_link_url` (`string`): A link straight to this property in the Greenfinch app, for a person following up from inside your own system. - `property.lease_type` (`string | null`): What Greenfinch's research believes the property's lease arrangement is — triple_net, gross, modified_gross, ground_lease, sale_leaseback or owner_occupied — because it says who usually pays for upkeep. Recorded only when a page research read (a leasing flyer, a listing, an article) states it; the property's detail carries that page and its words. Optional: Research has not found a page that states the lease arrangement, which is most properties; the lease-type field is not switched on yet; or the value is owner_occupied or sale_leaseback, which describe who owns the property and are withheld until its research is unlocked for you. - `organizations[].greenfinch_organization_id` (`string`): Greenfinch's identifier for the firm, stable across syncs and the key every link row uses. - `organizations[].role` (`string`): What the firm is to the property: owner, property_manager, operator (the business that runs a site it does not own and decides its upkeep), tenant (occupies space without deciding upkeep; recorded, never the buyer), or another attributed role. - `organizations[].attribution_type` (`string | null`): What kind of evidence attached the firm to the property, from strongest to weakest: named for the property itself, portfolio evidence, a matched roster, an inferred coverage area, or the county's record. Optional: The link predates Greenfinch recording how it was established. - `organizations[].is_current` (`boolean`): Whether the firm's relationship to the property still stands. - `organizations[].name` (`string | null`): The firm's name, resolved into one answer from everything Greenfinch holds. Optional: Greenfinch holds the firm as a linked record without a settled name. - `organizations[].domain` (`string | null`): The firm's own web domain. Optional: No domain was resolved for the firm. - `organizations[].location` (`object | null`): The firm's own address, as city, state and postal code. Optional: Greenfinch holds no address for the firm. - `organizations[].location.city` (`string | null`): The city of the firm's own address. Optional: Greenfinch holds no address for the firm. It is also empty whenever organizations[].location is. - `organizations[].location.state` (`string | null`): The state of the firm's own address. Optional: Greenfinch holds no address for the firm. It is also empty whenever organizations[].location is. - `organizations[].location.postal_code` (`string | null`): The postal code of the firm's own address. Optional: Greenfinch holds no address for the firm. It is also empty whenever organizations[].location is. - `organizations[].industry` (`string | null`): What the firm does, as one industry classification. Optional: No industry was resolved for the firm. - `organizations[].employees` (`number | null`): How many people the firm employs, as a single estimate. Optional: Only a range is known, or nothing is. - `organizations[].employees_range` (`string | null`): How many people the firm employs, as a band — used when no single figure is known. Optional: No headcount is known. - `organizations[].linkedin_handle` (`string | null`): The firm's LinkedIn handle. Optional: No LinkedIn page was resolved for the firm. - `organizations[].phone_numbers` (`string[] | null`): The firm's own main lines. These are business numbers, delivered as Greenfinch holds them — the phone policy that governs a person's numbers does not apply to a firm's. Optional: No number is held for the firm. - `contacts[].greenfinch_contact_id` (`string`): Greenfinch's identifier for the person, stable across syncs and the key every link row uses. - `contacts[].role` (`string | null`): What the person is to the property: owner, property_manager or operator by the company they work for, or facilities, procurement, leasing or other by their job. Optional: No role was attributed to this person for this property. - `contacts[].attribution_type` (`string | null`): What kind of evidence attached the person to the property. Optional: The link predates Greenfinch recording how it was established. - `contacts[].relationship_status` (`string`): Whether the person's connection to the property is active, has changed, is former, or is under review. - `contacts[].relationship_confidence` (`"high" | "medium" | "low" | string`): How strongly Greenfinch holds that connection — high, medium or low. - `contacts[].stale_pending_review` (`boolean`): True when Greenfinch's own checks suspect this connection has gone stale but have not concluded it. Treat the connection as under review rather than confirmed; it never silently becomes a confirmed one. - `contacts[].identity_not_confirmed` (`boolean`): True when Greenfinch could not confirm this person's current job: the title and employer are the ones research found, and nobody has confirmed them. Treat them as unconfirmed. - `contacts[].full_name` (`string | null`): The person's name. Optional: Greenfinch holds no name for the person. - `contacts[].title` (`string | null`): The person's job title, in full — never shortened. Optional: No title is held for the person. - `contacts[].email` (`string | null`): The person's work email address. Optional: No deliverable address is held for the person. - `contacts[].email_validation_status` (`"valid" | "unknown" | null`): What Greenfinch knows about that address: valid means confirmed deliverable, and unknown means found but not yet confirmed — a positive signal, not a negative one. Optional: No address is held for the person. - `contacts[].linkedin_url` (`string | null`): The person's LinkedIn profile. Optional: No profile was resolved for the person, or the one on record has not passed Greenfinch's LinkedIn checks. - `contacts[].employer_name` (`string | null`): Who the person works for. Optional: No employer is held for the person. - `contacts[].phones[].value` (`string`): A phone number for the person, in national display format. Each number passes Greenfinch's phone-delivery policy before it ships, so a number Greenfinch holds but may not deliver never appears here. - `contacts[].phones[].label` (`string | null`): The label Greenfinch stored with that number. It is unchanged since this shape was first published, so an existing mapping on it keeps working. Optional: The number was stored without a label. - `contacts[].phones[].type` (`string | null`): What kind of number it is — mobile, direct or office — so you can tell them apart without reading the label. Optional: The kind could not be inferred. - `contacts[].phones[].fallback_from_employer` (`boolean`): True on the one stand-in entry a person with no number of their own receives: their employer's main office line. False on every number the person actually owns. - `contacts[].phone_e164` (`string | null`): The person's primary deliverable number in the international format, for matching against numbers already in your own system. Optional: The person has no number Greenfinch may deliver, or their only number is their employer's main office line — deliberately left out here, because two colleagues would otherwise share a number and your own matching would read them as the same person. - `contacts_summary` (`object | null`): What a reduced record carries instead of people: how many are attached and, for each, a job title and what revealing would yield. Optional: The record is complete, which is the ordinary case: only a reduced record carries this summary. - `contacts_summary.total_count` (`number`): How many people are attached to the property. Optional: Only when contacts_summary is empty: The record is complete, which is the ordinary case: only a reduced record carries this summary. - `contacts_summary.contacts[].title` (`string | null`): One attached person's job title, with no name, no identifier and no way to reach them — enough to judge whether revealing the property is worth it. Optional: No title is held for that person. It is also empty whenever contacts_summary is. - `contacts_summary.contacts[].availability.email` (`boolean`): Whether revealing would yield an email address for that person. Optional: Only when contacts_summary is empty: The record is complete, which is the ordinary case: only a reduced record carries this summary. - `contacts_summary.contacts[].availability.phone` (`boolean`): Whether revealing would yield a phone number for that person. Optional: Only when contacts_summary is empty: The record is complete, which is the ordinary case: only a reduced record carries this summary. - `contacts_summary.contacts[].availability.linkedin` (`boolean`): Whether revealing would yield a LinkedIn profile for that person. Optional: Only when contacts_summary is empty: The record is complete, which is the ordinary case: only a reduced record carries this summary. - `pipeline.in_pipeline` (`boolean`): Whether your organization has actually put this property in its pipeline. False means the rest of this section is a default Greenfinch filled in, not something your team did. - `pipeline.status` (`string`): Where your team has this property in its own pipeline. - `pipeline.deal_value` (`number | null`): The deal value your team recorded. Optional: Nobody on your team has recorded one. - `pipeline.status_changed_at` (`string`): When that status last changed. - `pipeline.lost_reason` (`string | null`): The reason your team recorded for losing the deal. Optional: The deal is not marked lost, or no reason was recorded. - `pipeline.disqualified_reason` (`string | null`): The reason your team recorded for disqualifying the property. Optional: The property is not disqualified, or no reason was recorded. - `pipeline.assigned_rep` (`object | null`): The person on your team who owns this property in your pipeline. Optional: Nobody on your team owns the property. - `pipeline.assigned_rep.greenfinch_user_id` (`string`): Greenfinch's identifier for the person on your team who owns this property. Optional: Only when pipeline.assigned_rep is empty: Nobody on your team owns the property. - `pipeline.assigned_rep.name` (`string | null`): That person's name. Optional: Their Greenfinch account carries no name. It is also empty whenever pipeline.assigned_rep is. - `pipeline.assigned_rep.email` (`string | null`): That person's email address. Optional: Their Greenfinch account carries no email address. It is also empty whenever pipeline.assigned_rep is. - `notes[].id` (`string`): Greenfinch's identifier for one note your team wrote. - `notes[].author_name` (`string | null`): Who on your team wrote it. Optional: Their Greenfinch account carries no name. - `notes[].created_at` (`string`): When it was written. - `notes[].content` (`string`): The note itself, as written, with any mention of a colleague resolved to their name. - `notes_digest` (`string`): Every note on the property as one block of plain text in date order — including notes beyond those listed individually — for a system with one text field to put them in. - `actions[].id` (`string`): Greenfinch's identifier for one follow-up your team created. - `actions[].action_type` (`"follow_up" | "call" | "email" | "meeting" | "other"`): What kind of follow-up it is — a call, an email, a meeting or another kind. - `actions[].description` (`string | null`): What your team wrote about it. Optional: It was created without a description. - `actions[].due_at` (`string`): When it is due. - `actions[].status` (`"pending" | "completed" | "cancelled"`): Whether it is still pending, has been completed, or was cancelled. - `actions[].completed_at` (`string | null`): When it was completed. Optional: It has not been completed. - `actions[].assignee_name` (`string | null`): Who on your team it is assigned to. Optional: It is unassigned, or their Greenfinch account carries no name. - `actions[].created_by_name` (`string | null`): Who on your team created it. Optional: Their Greenfinch account carries no name. - `actions[].created_at` (`string`): When it was created. - `actions_digest` (`string`): The whole follow-up history as one block of plain text in date order — pending, completed and cancelled alike. ## 9. Identifiers - A property is identified only by its Greenfinch UUID. Store that UUID next to your own record and use it in every call. - Never identify a property by a county parcel number or any key you see in a URL. Only the UUID is supported, and anything else can change. - Firms, people, lists and webhook endpoints are also identified by UUIDs. In the lead bundle the identifiers are `greenfinch_property_id`, `organizations[].greenfinch_organization_id`, `contacts[].greenfinch_contact_id`, `pipeline.assigned_rep.greenfinch_user_id`. - When a property is merged after a county data refresh, calls return `409` `PROPERTY_RETIRED` with `successorPropertyId`. Replace the stored UUID with the successor. ## 10. Pagination `POST /properties/search` pages in one of two ways, depending on how you search: - With `query` (free text): cursor paging only. Pass back `next_cursor` as `cursor` until `next_cursor` is null. `offset` is ignored. - With a location filter or `bounds`: offset paging only. `next_cursor` is always null here, even when `has_more` is true. Pass `next_offset` as `offset` until `next_offset` is null. `offset` can be at most 10,000. These searches also return `total_matching`. - A cursor works only with the sort it came from; otherwise the call is refused with `CURSOR_SORT_MISMATCH`. A malformed cursor is `INVALID_CURSOR`. Either way, start again from the first page. Other tools and endpoints: - `limit` sets the page size. Each parameter description below gives the default and maximum. - Offset paging: `offset` is the number of rows to skip, so the next page is `offset` plus `limit`. - Cursor paging: pass back the cursor the previous response gave you, in the parameter of the same name. - Paging parameters by tool. `POST /properties/search`, `POST /contacts/browse`, `POST /organizations/browse` take the same ones as their MCP twins. - `greenfinch_search_properties`: `limit` (Max results, default 50, max 200.), `cursor` (Pagination cursor from a prior `query`-mode call.), `offset` (Rows to skip in a county/bounds search — page 2 of a ranked list is offset=limit. (min 0, max 10000)) - `greenfinch_get_pipeline`: `limit` (Max rows, default 200, max 200.), `offset` (Rows to skip (pages are newest-change first).) - `greenfinch_run_saved_search`: `limit` (Max rows, default 50, max 200.), `offset` (Rows to skip — requires sortBy.) - `greenfinch_get_lead_bundles`: `limit` (Bundles per page, default 25, max 50.), `offset` (List members to skip (pages are list order).) - `greenfinch_browse_organizations`: `limit` (Max rows, default 25, max 200.), `offset` (Rows to skip.) - `greenfinch_get_property_constituents`: `limit` (Max rows, default 100, max 200.), `offset` (Rows to skip.) - `greenfinch_get_property_activity`: `limit` (Max rows, default 50, max 200.), `offset` (Rows to skip.) - `greenfinch_browse_contacts`: `limit` (Max rows, default 25, max 200.), `offset` (Rows to skip.) - `greenfinch_get_reveal_history`: `limit` (Max rows, default 50, max 200.), `offset` (Rows to skip.) - `greenfinch_get_changes`: `limit` (Max rows, default 50, max 200.), `offset` (Rows to skip.) ## 11. Do not - Do not work around a limit: no extra keys to multiply the rate, no parallel workers to beat a daily limit, no retry loops that ignore `Retry-After`. Wait, or ask an admin. - Do not share keys. Give each system its own key with only the scopes it needs, keep keys in a secrets manager, and never put one in client-side code, a repository or a prompt. - Do not scrape the Greenfinch app, or drive a browser signed in as a person to read pages. Use this API, within your allowance. - Do not store a property under anything but its Greenfinch UUID, and do not branch on error message text. - Do not spend credits the user did not ask for. Unlocking and revealing cost credits; check the cost first.