# Greenfinch essentials for coding agents This is the short version of the Greenfinch integration guide for coding agents: what a first integration needs. It is generated from the Greenfinch developer documentation. 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. ## 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.