# Errors (/api-reference/errors) Errors are JSON with a stable `error` code. Most also have a human-readable `message`, and some add fields (`max`, `states`, `type`, `fieldgeist_id`): ```json { "error": "insufficient_credit", "message": "This team is out of credit. Add credits at https://app.fieldgeist.com/billing." } ``` Branch on the HTTP status and `error`. Don't parse `message`: its wording can change. Errors aren't billed. Only `200` answers are. ## Order of checks [#order-of-checks] A request is checked in this order, and the first failure is the answer: 1. **API key** (`401`, `429`, `503 unavailable`). 2. **Team status** (`403 team_paused`, then `402`). These are checked before the body, so a paused team gets `403` even for a malformed request. 3. **Request** (`400`, `413`, `422`). 4. **Answer** (`200`, `404`, `500`, `503 matchkey_unavailable`). ## All errors [#all-errors] | Status | `error` | Endpoints | Cause | What to do | | --- | --- | --- | --- | --- | | `400` | `invalid_json` | Parse, Lookup | The body isn't valid JSON. | Fix the body. | | `400` | `line_required` | Parse | The body has no `line` string. | Send `{"line": "..."}`. | | `400` | `components_required` | Lookup | The body isn't an object, or a component isn't a string or number. | Send components or a parse response. | | `400` | `street_required` | Lookup | No `streetName`. | Add the street. | | `400` | `location_required` | Lookup | No `zip5` or `city`; or a city in more than one state with no `state` (adds `states`, the number of states). | Add `zip5`, or `city` and `state`. | | `400` | `invalid_id` | Lookup by ID | The ID isn't `adr_` plus a 26-character ULID. | Check the ID. | | `401` | `invalid_api_key` | All | The `Authorization` header is malformed, or the key is unknown or revoked. Comes with `WWW-Authenticate: Bearer`. | Send `Authorization: Bearer ` with an active key. | | `402` | `insufficient_credit` | All | The team's credit balance is used up. | Add credits in the portal's **Billing** page, or turn on auto recharge. Requests resume within minutes. | | `402` | `budget_exceeded` | All | The team reached the monthly budget it set. | Raise or remove the budget in **Billing**. Budgets count spend per calendar month (UTC). | | `403` | `team_paused` | All | Fieldgeist paused the team's API access. | Contact support@fieldgeist.com. | | `404` | `not_found` | Lookup by ID | No parcel has this ID. Echoes `fieldgeist_id`. Not billed. | Look the address up again. | | `413` | `line_too_long` | Parse | `line` is longer than 256 characters. Adds `max`. | Shorten the line. | | `422` | `unsupported_type` | Lookup | The address `type` isn't `street`. Echoes `type`. | Only street addresses have parcels. | | `422` | `unsupported_id_type` | Lookup by ID | A building ID (`bld_…`). | Use a parcel's `adr_` ID. | | `429` | `rate_limited` | All | Too many different unrecognized keys from your IP address in a minute. | Fix the key, wait a minute. See [Rate limits](/rate-limits). | | `500` | `internal_error` | All | Something failed on our side. | Retry with backoff. If it persists, contact support with the `x-request-id`. | | `503` | `unavailable` | All | We couldn't check the API key. | Retry shortly. | | `503` | `matchkey_unavailable` | Lookup, Lookup by ID | The parcel index is unavailable. | Retry later. | A request to a path that doesn't exist (or with the wrong method, such as `GET /v1/verify/parse`) gets `404` with a plain-text body, not JSON. ## 402 and 403 in detail [#402-and-403-in-detail] The three refusals for a valid key: ```json { "error": "insufficient_credit", "message": "This team is out of credit. Add credits at https://app.fieldgeist.com/billing." } ``` ```json { "error": "budget_exceeded", "message": "This team reached its monthly budget. Raise it at https://app.fieldgeist.com/billing." } ``` ```json { "error": "team_paused", "message": "This team's API access is paused. Contact support@fieldgeist.com." } ``` - Refused requests appear in the portal's **Logs** with their error code and request ID. They aren't billed. - A team's billing status is updated shortly after the spend that changes it, so a few requests can be answered after the balance reaches zero. After you add credits, requests resume within minutes. ## Retrying [#retrying] Every Verify endpoint only reads data, so retrying is safe. Note that each `200` is billed, including a retried one. | Status | Retry? | | --- | --- | | `400`, `401`, `404`, `413`, `422` | No. Fix the request. | | `402`, `403` | Not until the team's billing or status changes. | | `429` | After a minute, with a valid key. | | `500`, `503` | Yes, with exponential backoff. | ## Request IDs [#request-ids] Every request made with a valid key, including refused ones, gets an `x-request-id` response header (`req_` followed by 26 characters). The same ID is on the request's row in **Logs**. Include it when you write to support@fieldgeist.com. # Look up by Fieldgeist ID (/api-reference/lookup-by-id) ```http GET https://api.fieldgeist.com/v1/verify/lookup/{fieldgeist_id} ``` Returns one parcel by its [Fieldgeist ID](/concepts/fieldgeist-ids): its address, APN, county FIPS code, coordinates and building ID. Use it to refresh a parcel you matched earlier without matching the address again. ## Authentication [#authentication] `Authorization: Bearer `. See [Authentication](/authentication). ## Path parameters [#path-parameters] | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `fieldgeist_id` | string | Yes | A parcel's Fieldgeist ID: `adr_` followed by a 26-character ULID, such as `adr_01JA2Z3Y4X5W6V7T8S9R0QPNMA`. Building IDs (`bld_…`) can't be fetched yet. | ## Request example [#request-example] ```bash curl https://api.fieldgeist.com/v1/verify/lookup/adr_01JA2Z3Y4X5W6V7T8S9R0QPNMA \ -H "Authorization: Bearer $FIELDGEIST_API_KEY" ``` ```js const id = "adr_01JA2Z3Y4X5W6V7T8S9R0QPNMA"; const res = await fetch(`https://api.fieldgeist.com/v1/verify/lookup/${id}`, { headers: { Authorization: `Bearer ${process.env.FIELDGEIST_API_KEY}` }, }); if (res.status === 404) { // No parcel has this ID. } const parcel = await res.json(); ``` ```python import os import requests fieldgeist_id = "adr_01JA2Z3Y4X5W6V7T8S9R0QPNMA" res = requests.get( f"https://api.fieldgeist.com/v1/verify/lookup/{fieldgeist_id}", headers={"Authorization": f"Bearer {os.environ['FIELDGEIST_API_KEY']}"}, timeout=10, ) if res.status_code == 404: parcel = None # No parcel has this ID. else: res.raise_for_status() parcel = res.json() ``` ## Response [#response] Parcel values on this page are illustrative. ### 200 OK [#200-ok] ```json { "status": "active", "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S9R0QPNMA", "superseded_by": [], "building_id": null, "apn": "198520-0495", "fips": "53033", "lat": 47.620494, "lon": -122.349235, "addressNumber": "400", "addressNumberSuffix": null, "predirectional": null, "streetName": "BROAD", "streetSuffix": "ST", "postdirectional": null, "occupancyId": null, "city": "SEATTLE", "state": "WA", "zip5": "98109", "zip4": null } ``` | Field | Type | Description | | --- | --- | --- | | `status` | string | `active`, or `superseded` when the parcel was split or merged and other IDs replace it. | | `fieldgeist_id` | string | The ID you asked for. | | `superseded_by` | string[] | The IDs that replace this one. Empty while `active`. | | `building_id` | string \| null | The building's ID (`bld_…`) for a unit parcel. | | `apn` | string \| null | The county's assessor parcel number. | | `fips` | string \| null | 5-digit county FIPS code. | | `lat`, `lon` | number \| null | Coordinates of the parcel. | | `addressNumber`, `addressNumberSuffix`, `predirectional`, `streetName`, `streetSuffix`, `postdirectional`, `occupancyId`, `city`, `state`, `zip5`, `zip4` | string \| null | The parcel's address as the index records it, in Publication 28 form. | ### A superseded parcel [#a-superseded-parcel] ```json { "status": "superseded", "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S04242ZZZ", "superseded_by": ["adr_01JA2Z3Y4X5W6V7T8S99998ZZZ", "adr_01JA2Z3Y4X5W6V7T8S99999ZZZ"], "building_id": null, "apn": "SYN-4242", "...": "..." } ``` A superseded ID still answers with its last known record. Fetch each ID in `superseded_by` to get the current parcels. ### Errors [#errors] | Status | `error` | Cause | | --- | --- | --- | | `400` | `invalid_id` | The ID isn't `adr_` followed by a 26-character ULID. | | `404` | `not_found` | No parcel has this ID. The response echoes `fieldgeist_id`. Not billed. | | `422` | `unsupported_id_type` | A building ID (`bld_…`). Building IDs can't be fetched yet; use a parcel's `adr_` ID. | | `503` | `matchkey_unavailable` | The parcel index is unavailable. Retry later. | | `401`, `402`, `403`, `429`, `5xx` | | See [Errors](/api-reference/errors). | ```json { "error": "not_found", "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S9R0QPZZZ" } ``` # Look up a parcel (/api-reference/lookup) ```http POST https://api.fieldgeist.com/v1/verify/lookup ``` Matches a street address to a parcel and returns its Fieldgeist ID, county APN, county FIPS code and coordinates. When the address fits more than one parcel, or the building has unit parcels and no unit was given, the answer says so (`ambiguous`, `unit_required`) and lists candidates instead of picking one. Send either the components of an address or a whole [parse](/api-reference/parse) response. ## Authentication [#authentication] `Authorization: Bearer `. See [Authentication](/authentication). ## Request body [#request-body] The body is a JSON object in one of two forms: - **A parse response**, unchanged: `{"type": "street", "components": {...}, ...}`. Only `type` and `components` are read. - **Bare components**: `{"addressNumber": "400", "streetName": "BROAD", ...}`, optionally with `"type"`. Without `type`, the address is treated as a street address. | Field | Type | Required | Description | | --- | --- | --- | --- | | `streetName` | string | Yes | Street name. | | `zip5` | string | One of `zip5` or `city` | 5-digit ZIP Code. The fastest and most precise way to locate the address. | | `city` | string | One of `zip5` or `city` | City. With `state`, or alone when the city's name exists in only one state. | | `state` | string | No | Two-letter state code. Needed with `city` when the city's name exists in more than one state. | | `addressNumber` | string | No | House number. Without it, the answer can only be a parcel with no house number (`no_situs_parcel`) or `not_found`. | | `addressNumberSuffix` | string | No | Such as `1/2`. | | `predirectional`, `postdirectional` | string | No | Street directions, such as `W` or `NW`. | | `streetSuffix` | string | No | Street type, such as `ST`. Leave it out only if you don't know it: without it, every suffix at that house number is a candidate. | | `occupancyType`, `occupancyId` | string | No | Unit designator and number. The unit number picks a unit parcel in a building that has them. | | `zip4` | string | No | Accepted, not used for matching. | | `type` | string | No | Address type from a parse. Anything other than `street` is refused with `422 unsupported_type`. | Values can be strings or numbers. Case and punctuation don't matter: values are normalized the same way the parcel index is. ## Request example [#request-example] ```bash curl https://api.fieldgeist.com/v1/verify/lookup \ -H "Authorization: Bearer $FIELDGEIST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "addressNumber": "400", "streetName": "BROAD", "streetSuffix": "ST", "city": "SEATTLE", "state": "WA", "zip5": "98109" }' ``` ```js const res = await fetch("https://api.fieldgeist.com/v1/verify/lookup", { method: "POST", headers: { Authorization: `Bearer ${process.env.FIELDGEIST_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ addressNumber: "400", streetName: "BROAD", streetSuffix: "ST", city: "SEATTLE", state: "WA", zip5: "98109", }), }); const match = await res.json(); ``` ```python import os import requests res = requests.post( "https://api.fieldgeist.com/v1/verify/lookup", headers={"Authorization": f"Bearer {os.environ['FIELDGEIST_API_KEY']}"}, json={ "addressNumber": "400", "streetName": "BROAD", "streetSuffix": "ST", "city": "SEATTLE", "state": "WA", "zip5": "98109", }, timeout=10, ) res.raise_for_status() match = res.json() ``` ## Response [#response] Parcel values on this page (IDs, APNs, coordinates) are illustrative. ### 200 OK: matched [#200-ok-matched] ```json { "status": "matched", "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S9R0QPNMA", "building_id": null, "apn": "198520-0495", "fips": "53033", "lat": 47.620494, "lon": -122.349235, "matchConfidence": 1, "candidates": [], "searched": ["matchkey/v3.2/zip5=98109/part.parquet"], "readPath": "chunk", "fuzzy": false } ``` | Field | Type | Description | | --- | --- | --- | | `status` | string | The outcome. See [Statuses](#statuses). | | `fieldgeist_id` | string \| null | The matched parcel's [Fieldgeist ID](/concepts/fieldgeist-ids) (`adr_…`). `null` when there is no single parcel. | | `building_id` | string \| null | The building's ID (`bld_…`) when the address has unit parcels. See [Condos and units](/concepts/condos-and-units). | | `apn` | string \| null | The county's assessor parcel number, as the county writes it. | | `fips` | string \| null | 5-digit county FIPS code. | | `lat`, `lon` | number \| null | Coordinates of the parcel. | | `matchConfidence` | number | 0 to 1. How closely the input matched the parcel. `0` when there's no single parcel (`ambiguous`, `not_found`); each candidate then has its own. See [Match confidence](/concepts/confidence-and-warnings#match-confidence). | | `unit_required` | boolean | Present and `true` when the address has unit parcels and the input named no unit that exists. | | `unit_count` | number | Present with `unit_required`: how many unit parcels the address has. | | `candidates` | object[] | Other parcels that fit, up to 10. Empty for a clean single match. See [Candidates](#candidates). | | `fuzzy` | boolean | `true` when the street name matched as a near miss (a typo or variant) rather than exactly. Confidence is lowered. | | `state_inferred` | string | Present when you sent a city without a state or ZIP: the state the city was found in. | | `searched` | string[] | Diagnostic: which parts of the index were read. | | `readPath` | string | Diagnostic: how the answer was found (`chunk`, `fuzzy_chunk`, `whole`, `fuzzy`, `none`). | ### Statuses [#statuses] | `status` | Meaning | Top-level parcel fields | What to do | | --- | --- | --- | --- | | `matched` | One parcel fits. | Set | Use it. Check `unit_required` and `fuzzy`. | | `matched_building` | The building matched, but not a unit parcel. Either the input named a unit that has no parcel of its own (the answer is the building's parcel), or the building has only unit parcels and the input named none of them (no top-level parcel; the units are the candidates). | The building's parcel, or `null` with unit candidates | Use the building's parcel if that's enough. If `unit_required` is `true`, ask for the unit. | | `ambiguous` | Two or more different parcels fit equally well, for example `3 SPRUCE` when there's a `3 SPRUCE ST` and a `3 SPRUCE CT`. | `null` | Show the candidates, or ask for the missing part (usually the suffix or a direction). | | `no_situs_parcel` | A weak match. The input had no house number, so the answer is a parcel on that street; or, in a single-ZIP search, the house number isn't on the street and the street has exactly one parcel with no house number on record. | Set | Don't treat it as a match without checking. `matchConfidence` is low. | | `not_found` | No parcel fits. | `null` | Check the input. Billed like any answer. | ### 200 OK: ambiguous [#200-ok-ambiguous] ```json { "status": "ambiguous", "fieldgeist_id": null, "building_id": null, "apn": null, "fips": null, "lat": null, "lon": null, "matchConfidence": 0, "candidates": [ { "apn": "1234-0001", "fips": "53033", "lat": 47.611, "lon": -122.335, "zip5": "98101", "addressNumber": "3", "streetName": "SPRUCE", "streetSuffix": "CT", "occupancyId": null, "matchConfidence": 0.85, "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S9R0QPSP1", "building_id": null }, { "apn": "1234-0002", "fips": "53033", "lat": 47.612, "lon": -122.336, "zip5": "98101", "addressNumber": "3", "streetName": "SPRUCE", "streetSuffix": "ST", "occupancyId": null, "matchConfidence": 0.85, "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S9R0QPSP2", "building_id": null } ], "searched": ["matchkey/v3.2/zip5=98101/part.parquet"], "readPath": "chunk", "fuzzy": false } ``` The input was `{"addressNumber": "3", "streetName": "SPRUCE", "zip5": "98101"}`. With `"streetSuffix": "CT"` the same request returns `matched`. ### 200 OK: unit required [#200-ok-unit-required] A condo building whose units are separate parcels, looked up without a unit: ```json { "status": "matched_building", "fieldgeist_id": null, "building_id": "bld_01JA2Z3Y4X5W6V7T8S9R0QPNMC", "apn": null, "fips": null, "lat": null, "lon": null, "matchConfidence": 0.8, "unit_required": true, "unit_count": 2, "candidates": [ { "apn": "777-1", "fips": "53033", "lat": 47.61, "lon": -122.31, "zip5": "98109", "addressNumber": "600", "streetName": "PINE", "streetSuffix": "ST", "occupancyId": "1", "matchConfidence": 0.8, "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S9R0QPNMD", "building_id": "bld_01JA2Z3Y4X5W6V7T8S9R0QPNMC" }, { "apn": "777-2", "fips": "53033", "lat": 47.61, "lon": -122.31, "zip5": "98109", "addressNumber": "600", "streetName": "PINE", "streetSuffix": "ST", "occupancyId": "2", "matchConfidence": 0.8, "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S9R0QPNME", "building_id": "bld_01JA2Z3Y4X5W6V7T8S9R0QPNMC" } ], "searched": ["matchkey/v3.2/zip5=98109/part.parquet"], "readPath": "chunk", "fuzzy": false } ``` Sending `"occupancyId": "2"` returns `matched` with that unit's parcel (`adr_…NME`) and the same `building_id`. A building that has its own parcel *and* unit parcels returns `matched` with the building's parcel plus `unit_required: true`. See [Condos and units](/concepts/condos-and-units). `unit_required` can come with `matched` or `matched_building`. Check the flag, not the status. ### Candidates [#candidates] Each candidate is a parcel that fits the input: | Field | Type | Description | | --- | --- | --- | | `fieldgeist_id` | string \| null | The parcel's Fieldgeist ID. | | `building_id` | string \| null | Its building's ID, for unit parcels. | | `apn`, `fips`, `lat`, `lon` | | As at the top level. | | `zip5`, `addressNumber`, `streetName`, `streetSuffix`, `occupancyId` | string \| null | The parcel's address as the index records it, so you can tell candidates apart. | | `matchConfidence` | number | How closely the input matched this candidate. | | `fuzzy` | boolean | Present and `true` when this candidate's street name was a near miss. | Candidates are capped at 10 per answer; `unit_count` gives the full number of units. ### City without a ZIP [#city-without-a-zip] Without `zip5`, the lookup searches the city. When the city's name exists in only one state, the state can be left out and the answer includes `state_inferred`: ```json { "status": "matched", "fieldgeist_id": "adr_…", "state_inferred": "TX", "...": "..." } ``` When the name exists in several states, the request is refused: ```json { "error": "location_required", "message": "city is in 3 states: add a state or zip5", "states": 3 } ``` ### Errors [#errors] | Status | `error` | Cause | | --- | --- | --- | | `400` | `invalid_json` | The body isn't valid JSON. | | `400` | `components_required` | The body isn't an object, or a component is not a string or number. | | `400` | `street_required` | No `streetName`. | | `400` | `location_required` | Neither `zip5` nor `city`, or a `city` that exists in more than one state without a `state`. The second case adds `states`. | | `422` | `unsupported_type` | `type` isn't `street` (PO boxes, rural routes and other types have no parcel). The response echoes `type`. | | `500` | `internal_error` | The lookup failed on our side. Retry. | | `503` | `matchkey_unavailable` | The parcel index is unavailable. Retry later. | | `401`, `402`, `403`, `429` | | See [Errors](/api-reference/errors). | ```json { "error": "unsupported_type", "type": "po_box", "message": "only type=street addresses can be matched to a parcel" } ``` # API overview (/api-reference/overview) ## Base URL [#base-url] ```text https://api.fieldgeist.com ``` All endpoints are served over HTTPS. ## Endpoints [#endpoints] | Method | Path | Does | | --- | --- | --- | | `POST` | [`/v1/verify/parse`](/api-reference/parse) | Free text to USPS-standard components | | `POST` | [`/v1/verify/lookup`](/api-reference/lookup) | Parsed address to parcel | | `GET` | [`/v1/verify/lookup/{fieldgeist_id}`](/api-reference/lookup-by-id) | One parcel by its Fieldgeist ID | Only answers with HTTP status `200` are billed. See [Credits and billing](/billing), and the [pricing page](https://fieldgeist.com/pricing) for rates. ## Requests [#requests] - **Authentication:** `Authorization: Bearer ` on every request. See [Authentication](/authentication). - **Bodies:** `POST` endpoints take a JSON body. Send `Content-Type: application/json`. - **Responses:** JSON, including every error from these endpoints. Errors share one shape: ```json { "error": "location_required", "message": "zip5, city+state, or a city whose name is in only one state" } ``` `error` is a stable, machine-readable code; branch on it. `message` is for people and may change. Some errors add fields, such as `max` on `line_too_long`. See [Errors](/api-reference/errors). ## Response headers [#response-headers] | Header | On | Meaning | | --- | --- | --- | | `x-request-id` | Every request made with a valid key | The request's ID (`req_` followed by 26 characters). The same ID appears on the request's row in the portal's **Logs**. Include it when you contact support. | | `Server-Timing` | Successful answers | Where the time went on our side, such as `inference;dur=…` for parse and `r2fetch;dur=…`, `match;dur=…` and `total;dur=…` for lookups. For diagnostics only: the metric names can change. | | `WWW-Authenticate` | `401` responses | `Bearer`. | ## Paths and versions [#paths-and-versions] The version is in the path (`/v1`). Fieldgeist gives at least 30 days' notice by email before retiring an API that teams are using (see the [Terms](https://fieldgeist.com/terms)). Two older paths are still served and behave exactly like the current ones. Use the `/v1/verify/...` paths in new code. | Older path | Current path | | --- | --- | | `POST /v1/address/parse` | `POST /v1/verify/parse` | | `POST /v1/property/lookup` | `POST /v1/verify/lookup` | | `GET /v1/property/lookup/{fieldgeist_id}` | `GET /v1/verify/lookup/{fieldgeist_id}` | ## Calling from a browser [#calling-from-a-browser] The API is meant to be called from your servers. It doesn't send CORS headers for your origins, so browser code on your site can't call it directly, and a key in browser code would be public anyway. ## Data conventions [#data-conventions] - Address components are uppercase strings, as USPS Publication 28 writes them (`"AVE"`, `"NW"`, `"APT"`). - Component keys are camelCase (`addressNumber`, `zip5`). Parcel identifiers are snake_case (`fieldgeist_id`, `building_id`). - Fields that have no value are `null` in lookup responses and are left out of parse `components`. - Coordinates are decimal degrees (`lat`, `lon`). # Parse an address (/api-reference/parse) ```http POST https://api.fieldgeist.com/v1/verify/parse ``` Sends one free-text US address to [US-LEX-1](/concepts/model), the Verify model. The response gives the address type, its components in USPS Publication 28 form, the standardized delivery lines, a confidence score and warnings for anything missing, unusual or out of scope. The parser standardizes what you send (`Avenue` becomes `AVE`, `apt` becomes `APT`); it doesn't correct spelling or check that the address exists. To match the result to a parcel, send it to [Look up a parcel](/api-reference/lookup). ## Authentication [#authentication] `Authorization: Bearer `. See [Authentication](/authentication). ## Request body [#request-body] | Field | Type | Required | Description | | --- | --- | --- | --- | | `line` | string | Yes | The address as one line of text, up to 256 characters. Commas and line breaks are optional. | ## Request example [#request-example] ```bash curl https://api.fieldgeist.com/v1/verify/parse \ -H "Authorization: Bearer $FIELDGEIST_API_KEY" \ -H "Content-Type: application/json" \ -d '{"line": "350 5th ave apt 21b new york ny 10118"}' ``` ```js const res = await fetch("https://api.fieldgeist.com/v1/verify/parse", { method: "POST", headers: { Authorization: `Bearer ${process.env.FIELDGEIST_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ line: "350 5th ave apt 21b new york ny 10118" }), }); if (!res.ok) { const { error, message } = await res.json(); throw new Error(`${res.status} ${error}: ${message ?? ""}`); } const parsed = await res.json(); ``` ```python import os import requests res = requests.post( "https://api.fieldgeist.com/v1/verify/parse", headers={"Authorization": f"Bearer {os.environ['FIELDGEIST_API_KEY']}"}, json={"line": "350 5th ave apt 21b new york ny 10118"}, timeout=10, ) res.raise_for_status() parsed = res.json() ``` ## Response [#response] ### 200 OK [#200-ok] ```json { "input": "350 5th ave apt 21b new york ny 10118", "type": "street", "components": { "city": "NEW YORK", "addressNumber": "350", "streetSuffix": "AVE", "streetName": "5TH", "occupancyType": "APT", "occupancyId": "21B", "state": "NY", "zip5": "10118" }, "lines": ["350 5TH AVE APT 21B", "NEW YORK NY 10118"], "warnings": [], "confidence": 0.9999020747368791, "model": { "name": "us-lex-1", "version": "0.1.0", "backend": "model" } } ``` | Field | Type | Description | | --- | --- | --- | | `input` | string | The `line` you sent, unchanged. | | `type` | string | Address type: `street`, `po_box`, `rural_route`, `military`, `general_delivery`, `dual` (a street address and a PO box in one line) or `ambiguous` (the type couldn't be determined). Only `street` addresses can be [looked up](/api-reference/lookup). | | `components` | object | The address components found, in Publication 28 form. Components that weren't found are left out. See [Components](#components). | | `lines` | string[] | The standardized address as delivery lines: the delivery line(s) first, then `CITY ST ZIP` (two spaces before the ZIP, as Publication 28 prefers). | | `warnings` | object[] | Anything missing, unusual or out of scope. Empty when the parse is clean. See [Warnings](#warnings). | | `confidence` | number | 0 to 1. How sure the model is of the labels it assigned. See [Confidence and warnings](/concepts/confidence-and-warnings). | | `model` | object | Which model read the line: `name` (`us-lex-1`), `version`, and `backend`: `model`, or `heuristic` when the rule-based fallback (`us-lex-rules`) answered instead. Store `version` with results you keep. See [Versions](/concepts/model#versions). | ### Components [#components] All values are uppercase strings. | Component | Example | Description | | --- | --- | --- | | `addressNumber` | `350` | Primary house number. | | `addressNumberSuffix` | `1/2` | Suffix after the house number, such as a fraction. | | `predirectional` | `W` | Direction before the street name. | | `streetName` | `5TH` | Street name. | | `streetSuffix` | `AVE` | Street type, abbreviated per Publication 28 (`ST`, `AVE`, `BLVD`). | | `postdirectional` | `NW` | Direction after the street name. | | `occupancyType` | `APT` | Secondary unit designator (`APT`, `STE`, `UNIT`, `#`). | | `occupancyId` | `21B` | Secondary unit number. | | `secondaryUnits` | `[{"type":"BLDG","id":"A"},{"type":"APT","id":"4"}]` | Present only when the address has two or more units, in input order. `occupancyType` and `occupancyId` then hold the last one. | | `poBoxType`, `poBoxId` | `PO BOX`, `1234` | PO box designator and number. | | `ruralRouteType`, `ruralRouteId` | `RR`, `2 BOX 15` | Rural route designator, and the route and box number. | | `firm` | | A business or recipient name in the line. | | `urbanization` | | Puerto Rico urbanization name. | | `city` | `NEW YORK` | City. | | `state` | `NY` | Two-letter state or territory code. | | `zip5` | `10118` | 5-digit ZIP Code. | | `zip4` | `0003` | ZIP+4 add-on. | | `other` | | Tokens the model couldn't place. Kept for inspection, never rendered into `lines`. | ### Warnings [#warnings] Each warning has a `code` and a `message`. Warnings about one part of the input also carry `start` and `end`, character offsets into `input`. Branch on `code`; the `message` wording may change. | `code` | Meaning | | --- | --- | | `missing_address_number` | A street name was found without a house number. | | `missing_street_name` | No street name was found (for example, a suffix with no street name before it). | | `missing_city` | No city. | | `missing_state` | No state. | | `missing_zip` | No ZIP Code. | | `invalid_zip` | The ZIP Code doesn't have 5 digits, or the ZIP+4 add-on doesn't have 4. | | `invalid_state` | The state isn't a Publication 28 state or possession abbreviation. | | `unit_without_id` | A unit designator (`APT`) with no unit number. | | `unit_id_without_type` | A unit number with no designator. `occupancyType` is set to `#`. | | `unknown_suffix` | The street suffix isn't a Publication 28 suffix. | | `unknown_unit_designator` | The unit designator isn't a Publication 28 designator; it's kept as typed. | | `unknown_tokens` | Some words couldn't be placed in any component. They're listed in `components.other`. | | `discontiguous_component` | One component appeared in separate parts of the line; the parts were joined with a space. | | `leading_zero_removed` | Leading zeros were removed from a PO box number. | | `line_overflow` | A line in `lines` is longer than 40 characters, the Publication 28 maximum. | | `dual_address` | The line has both a street address and a PO box or rural route. | | `ambiguous_type` | The address type couldn't be determined, or a word read as the street suffix isn't a Publication 28 suffix, so the street/city boundary is a guess. | | `ambiguous_city_street` | A word could belong to the street name or the city; the boundary between them is a guess. | | `unsupported_intersection` | The line looks like a street intersection, not a delivery address. | | `possibly_foreign` | The line looks like a non-US address. | | `low_confidence` | `confidence` is below 0.6. | | `empty_input` | `line` was empty or only whitespace. | | `model_unavailable` | The model couldn't run, so the rule-based fallback answered. | | `model_output_invalid` | The model's labels were inconsistent, so the rule-based fallback's parse was used. | ### A parse with warnings [#a-parse-with-warnings] An intersection parses, but the answer says it isn't a delivery address: ```json { "input": "Main St and 5th Ave, Springfield IL", "type": "ambiguous", "components": { "firm": "MAIN ST AND", "city": "SPRINGFIELD", "streetSuffix": "AVE", "streetName": "5TH", "state": "IL" }, "lines": ["MAIN ST AND", "5TH AVE", "SPRINGFIELD IL"], "warnings": [ { "code": "unsupported_intersection", "message": "\"Main St and 5th Ave\" looks like a street intersection, not a delivery address", "start": 0, "end": 19 }, { "code": "ambiguous_type", "message": "address type could not be determined: the input looks like a street intersection" }, { "code": "missing_address_number", "message": "street name found without a primary address number" }, { "code": "missing_zip", "message": "no ZIP Code" }, { "code": "low_confidence", "message": "confidence 0.50 < 0.6" } ], "confidence": 0.5, "model": { "name": "us-lex-1", "version": "0.1.0", "backend": "model" } } ``` An empty line is answered with `200`, `type: "ambiguous"` and an `empty_input` warning, not an error. It is billed. ### Errors [#errors] | Status | `error` | Cause | | --- | --- | --- | | `400` | `invalid_json` | The body isn't valid JSON. | | `400` | `line_required` | The body has no `line` string. | | `413` | `line_too_long` | `line` is longer than 256 characters. The response includes `"max": 256`. | | `401`, `402`, `403`, `429`, `5xx` | | See [Errors](/api-reference/errors). | ```json { "error": "line_too_long", "max": 256 } ``` # Authentication (/authentication) Every Verify request carries an API key in the `Authorization` header: ```http Authorization: Bearer fg_live_... ``` ```bash curl https://api.fieldgeist.com/v1/verify/parse \ -H "Authorization: Bearer $FIELDGEIST_API_KEY" \ -H "Content-Type: application/json" \ -d '{"line": "200 W Washington St Ste 1 Phoenix AZ 85003"}' ``` ```js await fetch("https://api.fieldgeist.com/v1/verify/parse", { method: "POST", headers: { Authorization: `Bearer ${process.env.FIELDGEIST_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ line: "200 W Washington St Ste 1 Phoenix AZ 85003" }), }); ``` ```python requests.post( "https://api.fieldgeist.com/v1/verify/parse", headers={"Authorization": f"Bearer {os.environ['FIELDGEIST_API_KEY']}"}, json={"line": "200 W Washington St Ste 1 Phoenix AZ 85003"}, timeout=10, ) ``` ## API keys [#api-keys] - **Keys belong to a team**, not a person. Requests made with a key draw from that team's credits and show in its Usage and Logs. - **Format:** `fg_live_` followed by 43 letters and digits (51 characters in all). - **Shown once.** The portal shows a new key in full only when you create it. Afterwards it shows the name, the last four characters, when it was created and when it was last used. Fieldgeist stores only a hash of the key, so a lost key can't be recovered: create a new one. - **Who can manage keys:** every team role (Owner, Admin and Developer) can create, rename and revoke the team's keys. - **Up to 25 active keys per team.** Key names are up to 64 characters. - **Unused keys are flagged.** A key older than 30 days that hasn't been used in 30 days shows "Unused for N days" in the portal, so you can revoke it. ### Create a key [#create-a-key] 1. Sign in at [app.fieldgeist.com](https://app.fieldgeist.com) and open **API keys**. 2. Select **Create key** and name it after where it will run, such as "Production server" or "Staging worker". 3. Copy the key, store it in your secrets manager, and confirm **I've stored this key**. ### Revoke a key [#revoke-a-key] Open **API keys**, find the key by its name and last four characters, and revoke it. Requests with a revoked key get `401 invalid_api_key`. A revocation can take up to about a minute to reach every server; requests made with the key before then are billed to the team. ## Keep keys safe [#keep-keys-safe] - **Call the API from your server**, never from a browser or mobile app. Anything shipped to a client can be read. The API doesn't accept cross-origin browser requests from your sites. - **One key per environment or service.** Separate keys for production, staging and each job let you revoke one without touching the others, and make Logs easier to read. - **Keep keys out of source control.** Load them from environment variables or a secrets manager. - **Revoke a key you suspect has leaked**, then create a replacement. Requests made with a key before you revoke it are billed to your team. - **Revoke keys you no longer use.** The portal's "Unused for N days" flag shows candidates. ## Authentication errors [#authentication-errors] | Status | `error` | When | | --- | --- | --- | | `401` | `invalid_api_key` | The header is malformed (not `Bearer `), or the key is unknown or revoked. The response carries `WWW-Authenticate: Bearer`. | | `429` | `rate_limited` | Too many requests with keys we don't recognize came from your IP address. Wait a minute. See [Rate limits](/rate-limits). | | `503` | `unavailable` | We couldn't check the key. Retry shortly. | ```json { "error": "invalid_api_key", "message": "Send a valid API key as Authorization: Bearer ." } ``` A valid key can still be refused when the team can't use the API: see `402` and `403` in [Errors](/api-reference/errors). # Credits and billing (/billing) Verify is paid for with prepaid credits. There are no plans, seats or minimums, and every new account starts with free credits. Rates for each endpoint are on the [pricing page](https://fieldgeist.com/pricing). ## What's billed [#whats-billed] **Every answered request is billed; errors and refused requests are free.** Concretely, a request is billed when the API answers with HTTP `200`: | Answer | Billed | | --- | --- | | Parse `200`, including a parse with warnings or an empty line | Yes | | Lookup `200` with any status: `matched`, `matched_building`, `ambiguous`, `no_situs_parcel`, `not_found` | Yes | | Lookup by ID `200` (`active` or `superseded`) | Yes | | Lookup by ID `404 not_found` | No | | Any `4xx` or `5xx` error, including `402` and `403` refusals | No | Requests from the portal's **Verify** page (the Playground) are billed the same way as API requests. To spend less per address, store the `fieldgeist_id` from a lookup and refresh the parcel later with [Look up by ID](/api-reference/lookup-by-id), which costs less than a new lookup. ## Credits [#credits] - **One balance per team.** Everyone on a team draws from the same credits. Owners and Admins manage billing. - **Add credits by card** in the portal's **Billing** page, $5 to $5,000 at a time. Payments are processed by Stripe. - **Credits don't expire** while your team exists. Purchased credits aren't refundable except as the [refund policy](https://fieldgeist.com/terms#refunds) describes. - **The sign-up credits** are given once per person. ## Auto recharge [#auto-recharge] Optional. When the balance falls below a threshold you set, the team's saved card is charged the amount you set. The threshold is at least $5, and the amount is at least the threshold and at most $5,000. If a charge is declined, auto recharge pauses until you update the card. ## Budgets and alerts [#budgets-and-alerts] - **Monthly budget:** caps a calendar month's spend (UTC). When the month's spend reaches it, requests are refused with `402 budget_exceeded` until you raise or remove it. - **Spend alert:** an email to the team's Owners and Admins when the month's spend reaches an amount you choose, below the budget. ## When the balance runs out [#when-the-balance-runs-out] At zero, requests are refused with `402 insufficient_credit` instead of running up a balance. Add credits and they resume within minutes. Refused requests show in **Logs** and aren't billed. ## Track spend [#track-spend] - **Usage** shows requests and spend by hour and day, kept for 13 months. - **Logs** shows each request for 30 days, with its status, outcome and request ID. - **Billing** shows the balance, payments and the month's spend. # Changelog (/changelog) Changes that affect API requests or responses, newest first. Each entry is tagged **New**, **Model** (a new version of [US-LEX-1](/concepts/model)), **Improved**, **Fixed**, **Deprecated** or **Breaking**. We give at least 30 days' notice by email before retiring an API that teams are using. ## October 19, 2026 [#october-19-2026] **Model** - US-LEX-1 (`us-lex-1`) 0.1.0, the first public version of the Verify model. **New** - Fieldgeist Verify: `POST /v1/verify/parse`, `POST /v1/verify/lookup` and `GET /v1/verify/lookup/{fieldgeist_id}`. - API keys, usage, request logs and the Verify Playground in the portal at app.fieldgeist.com. # Condos and units (/concepts/condos-and-units) In many buildings, each unit is its own parcel with its own APN. An address without a unit number then doesn't identify one parcel. Verify doesn't pick a unit for you: it tells you a unit is needed and how many there are. ## The flags [#the-flags] | Field | Meaning | | --- | --- | | `unit_required: true` | The address has unit parcels, and the input named no unit that exists. | | `unit_count` | How many unit parcels the address has. Comes with `unit_required`. | | `building_id` | The ID the building's unit parcels share (`bld_…`). | `unit_required` is left out of the response when it doesn't apply. Check `match.unit_required === true` rather than the status: it can come with `matched` or with `matched_building`. ## What a lookup returns [#what-a-lookup-returns] | Input | Building has | Answer | | --- | --- | --- | | A unit that exists (`APT 2`) | Unit parcels | `matched`: the unit's parcel, its `fieldgeist_id` and the `building_id`. | | No unit | Its own parcel and unit parcels | `matched`: the building's parcel, plus `unit_required: true`, `unit_count` and the `building_id`. | | No unit, or a unit that doesn't exist | Only unit parcels | `matched_building`: no top-level parcel (`fieldgeist_id: null`), `unit_required: true`, `unit_count`, the `building_id`, and up to 10 units as candidates. | | A unit that doesn't exist (`APT Z`) | Its own parcel and unit parcels | `matched_building`: the building's parcel, `unit_required: true` and the `building_id`. | | A unit (`APT 5`) | No unit parcels | `matched_building`: the parcel at that address, without `unit_required`. The unit has no parcel of its own. | Units are matched by their number (`occupancyId`); the designator isn't used, so `APT 2`, `UNIT 2` and `STE 2` all match unit `2`. A compound unit such as `B 4` is tried as written, then by its last part (`4`), with a slightly lower confidence. ## Handling unit_required [#handling-unit_required] What you do depends on who's on the other end: - **A person filling in a form:** ask for the unit. `unit_count` tells you whether to offer a picker (a few units) or a text field (many). - **An agent:** say the address needs a unit and stop, rather than choosing a candidate. That's the behavior `unit_required` exists for. - **A batch job:** store the `building_id` and flag the row for review. If you only need the building, the `building_id` (and the building's parcel, when there is one) may be enough. ```js const match = await lookup(components); if (match.unit_required) { // Don't pick a unit. Ask for one, or keep the building. return { needsUnit: true, buildingId: match.building_id, units: match.unit_count }; } if (match.status === "matched") { return { fieldgeistId: match.fieldgeist_id }; } ``` ## Example [#example] `600 Pine St, Seattle WA 98109` without a unit, in a building with two unit parcels and no parcel of its own: ```json { "status": "matched_building", "fieldgeist_id": null, "building_id": "bld_01JA2Z3Y4X5W6V7T8S9R0QPNMC", "matchConfidence": 0.8, "unit_required": true, "unit_count": 2, "candidates": [ { "occupancyId": "1", "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S9R0QPNMD", "building_id": "bld_01JA2Z3Y4X5W6V7T8S9R0QPNMC", "...": "..." }, { "occupancyId": "2", "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S9R0QPNME", "building_id": "bld_01JA2Z3Y4X5W6V7T8S9R0QPNMC", "...": "..." } ], "...": "..." } ``` The same address with `"occupancyType": "UNIT", "occupancyId": "2"`: ```json { "status": "matched", "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S9R0QPNME", "building_id": "bld_01JA2Z3Y4X5W6V7T8S9R0QPNMC", "matchConfidence": 1, "candidates": [], "...": "..." } ``` Values are illustrative. The full response is in [Look up a parcel](/api-reference/lookup#200-ok-unit-required). # Confidence and warnings (/concepts/confidence-and-warnings) Verify reports uncertainty instead of hiding it. A parse carries a `confidence` and `warnings`; a lookup carries a `matchConfidence`, a `status` and flags such as `unit_required` and `fuzzy`. Read them together: a high score doesn't make a warning go away. ## Parse confidence [#parse-confidence] `confidence` is a number from 0 to 1: the model's average certainty about the label it gave each word (house number, street name, city, and so on). - It measures how sure the model is of **how it split the line**, not whether the address exists. A clean parse of an address that doesn't exist can score close to 1. - Below **0.6**, the answer adds a `low_confidence` warning. - When the parse is structurally wrong or out of scope (a suffix with no street name, an intersection, a non-US address), confidence is capped at **0.5**. - An empty line has confidence `0` and an `empty_input` warning. ## Warnings [#warnings] Each warning has a `code`, a `message`, and for problems in one part of the input, `start` and `end` offsets into `input`, so you can highlight the text in a UI. ```json { "code": "ambiguous_city_street", "message": "street name \"via christna\" runs into the city with no suffix and no separator; the boundary is a guess", "start": 5, "end": 23 } ``` Warnings fall into a few groups: | Group | Codes | Usually means | | --- | --- | --- | | Missing parts | `missing_address_number`, `missing_street_name`, `missing_city`, `missing_state`, `missing_zip`, `unit_without_id`, `unit_id_without_type` | The input is incomplete. A lookup may still work: it needs a street and a ZIP or city. | | Invalid values | `invalid_zip`, `invalid_state`, `unknown_suffix`, `unknown_unit_designator` | A value isn't in USPS Publication 28's tables. It's kept as typed. | | Guesses | `ambiguous_type`, `ambiguous_city_street`, `discontiguous_component`, `unknown_tokens`, `low_confidence` | The split is uncertain. Show it to a person, or ask for a cleaner input. | | Out of scope | `unsupported_intersection`, `possibly_foreign`, `dual_address`, `empty_input` | The line isn't a single US delivery address. | | Formatting | `leading_zero_removed`, `line_overflow` | Informational. | | Fallback | `model_unavailable`, `model_output_invalid` | The rule-based fallback answered instead of the model. | The full list with meanings is in [Parse an address](/api-reference/parse#warnings). The parser doesn't fix spelling. `1509 via christna vista ca` keeps `VIA CHRISTNA`; the lookup's near-miss matching is what connects it to the right street. ## Match confidence [#match-confidence] `matchConfidence` (on a lookup and on each candidate) is a number from 0 to 1 for how closely the input matched the parcel's address. It isn't a probability. - **1** means the house number, street name, and any suffix, directions and unit you sent all matched exactly. - It's **lower** when the match needed slack: the input left out a suffix or direction the parcel has, a suffix or direction differed, the unit matched only in part, or the street name was a near miss (`fuzzy: true`). - A building matched without the unit you named, or a condo building with no unit given, scores below a full match. - A `no_situs_parcel` answer always scores low. - When there's no single parcel (`ambiguous`, `not_found`), the top-level `matchConfidence` is `0`. Look at each candidate's score instead. Use `status` to decide what happened, and `matchConfidence` to rank or to flag answers for review. ## Fuzzy matches [#fuzzy-matches] When no street matches exactly, the lookup tries near-miss street names (typos and spelling variants). A hit sets `fuzzy: true` on the answer (and on matching candidates) and lowers `matchConfidence`: ```json { "status": "matched", "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S9R0QPSP1", "matchConfidence": 0.72, "fuzzy": true, "...": "..." } ``` Near-miss matching only applies to real near misses. A street that isn't close to any known street returns `not_found`. ## Explicit uncertainty [#explicit-uncertainty] Some answers are uncertain by design. They're answers, not errors, and they're billed: - `ambiguous`: more than one parcel fits. See [Look up a parcel](/api-reference/lookup#statuses). - `unit_required`: the building has unit parcels and the input didn't name one that exists. See [Condos and units](/concepts/condos-and-units). # Fieldgeist IDs (/concepts/fieldgeist-ids) Every parcel Verify can match has a **Fieldgeist ID**. Store it with your own record of the property: it's the key for fetching the parcel again and for joining your data to later answers. | | `fieldgeist_id` | `building_id` | | --- | --- | --- | | Identifies | One parcel: a lot, a building on its own parcel, or one unit that is its own parcel | A building whose units are separate parcels | | Prefix | `adr_` | `bld_` | | Example | `adr_01JA2Z3Y4X5W6V7T8S9R0QPNMA` | `bld_01JA2Z3Y4X5W6V7T8S9R0QPNMC` | | Returned by | Lookup (top level and candidates), Lookup by ID | Lookup (top level and candidates), Lookup by ID | | Fetch directly | [`GET /v1/verify/lookup/{fieldgeist_id}`](/api-reference/lookup-by-id) | Not yet (`422 unsupported_id_type`) | ## Format [#format] A prefix followed by a 26-character [ULID](https://github.com/ulid/spec) in Crockford base32 (digits and uppercase letters without I, L, O and U). Treat IDs as opaque strings: compare them whole and don't parse the characters after the prefix. ## fieldgeist_id vs building_id [#fieldgeist_id-vs-building_id] - A **single-family home** or any parcel without units has a `fieldgeist_id` and `building_id: null`. - In a **condo building**, each unit is its own parcel with its own `fieldgeist_id`, and all of them share one `building_id`. - When the building also has a parcel of its own, a lookup without a unit returns that parcel's `fieldgeist_id` and the units' shared `building_id`, with `unit_required: true`. - When the building has only unit parcels, a lookup without a unit returns `fieldgeist_id: null`, the `building_id`, and the units as candidates. `building_id` lets you group units of one building without matching each unit. See [Condos and units](/concepts/condos-and-units). ## Fieldgeist IDs vs APNs [#fieldgeist-ids-vs-apns] The `apn` is the county assessor's parcel number. It's useful for reading county records, but it's only unique within a county and its format varies by county. A Fieldgeist ID is unique across the whole index, has one format, and is the identifier the API takes back. ## When parcels change [#when-parcels-change] When a parcel is split or merged, its old ID is kept and marked superseded: [Lookup by ID](/api-reference/lookup-by-id) returns `"status": "superseded"` and the replacing IDs in `superseded_by`. # US-LEX-1 (/concepts/model) Fieldgeist is an AI lab building tools for real estate. Verify, its first tool, is built around **US-LEX-1**, a purpose-built neural model that Fieldgeist trains on real US addresses. The model reads a line of free text and decides what each part of it is: the house number, the street, the unit, the city. The [parse endpoint](/api-reference/parse) is how you send it addresses today. ## How it reads an address [#how-it-reads-an-address] ### Split [#split] The line is split into words and punctuation, keeping what you typed. ### Label [#label] The model gives every word one of 19 labels taken from USPS Publication 28. It reads the whole line at once, so context decides each label: in `350 5th ave apt 21b`, the words around `5th` make it the street name rather than a house number, and `21b` after `apt` is a unit number. ### Assemble [#assemble] The labeled words become components, standardized to Publication 28 abbreviations (`ave` becomes `AVE`) and rendered into delivery lines. This step follows fixed rules, so the same labels always produce the same address. ### Score [#score] `confidence` is the model's average certainty about the labels it gave. See [Confidence and warnings](/concepts/confidence-and-warnings). ## The labels [#the-labels] Each label becomes a component in the parse response. | Label | Component | Example | | --- | --- | --- | | Address number | `addressNumber` | `350` | | Address number suffix | `addressNumberSuffix` | `1/2` | | Predirectional | `predirectional` | `W` | | Street name | `streetName` | `5TH` | | Street suffix | `streetSuffix` | `AVE` | | Postdirectional | `postdirectional` | `NW` | | Occupancy type | `occupancyType` | `APT` | | Occupancy ID | `occupancyId` | `21B` | | PO box type | `poBoxType` | `PO BOX` | | PO box ID | `poBoxId` | `1234` | | Rural route type | `ruralRouteType` | `RR` | | Rural route ID | `ruralRouteId` | `2 BOX 15` | | Firm | `firm` | A business name in the line | | City | `city` | `NEW YORK` | | State | `state` | `NY` | | ZIP Code | `zip5` | `10118` | | ZIP+4 | `zip4` | `0003` | | Urbanization | `urbanization` | A Puerto Rico urbanization | | Other | `other` | Words the model couldn't place | ## Where it runs [#where-it-runs] The model runs at the edge, inside the request that asks for it. There is no separate model service or queue: every parse is read by the model before the response is sent. ## When the model doesn't answer [#when-the-model-doesnt-answer] If the model can't run, or its labels contradict each other, a rule-based parser answers instead, and the response says so: `model` names `us-lex-rules` with `backend: "heuristic"` and a warning (`model_unavailable` or `model_output_invalid`) gives the reason. The rule-based parser is less reliable on unusual lines, so treat its answers as you would a low confidence. ## Versions [#versions] Every parse response names the model that read the line. In the API, US-LEX-1 is `us-lex-1`: ```json "model": { "name": "us-lex-1", "version": "0.1.0", "backend": "model" } ``` Fieldgeist trains new versions of US-LEX-1 and releases them to the API. Each release is listed in the [changelog](/changelog), tagged **Model**. Responses keep the same fields from one version to the next; what can change is how a line is read. If you store parses, store `model.version` with them so you know which reading you have. ## How the model improves [#how-the-model-improves] Fieldgeist retrains the model on new address data. Misreads found in evaluation are reviewed by people before each training round. If Verify misreads an address, email support@fieldgeist.com with the request's `x-request-id` header (see [Request IDs](/api-reference/errors#request-ids)) and what you expected. ## What the model doesn't do [#what-the-model-doesnt-do] - **It doesn't correct spelling.** It labels what you sent. Near-miss street names are handled by the [lookup](/api-reference/lookup), which flags them with `fuzzy: true`. - **It doesn't check that an address exists.** A well-formed address that doesn't exist can parse with high confidence. To match an address to a real parcel, use the lookup, which checks it against a parcel index Fieldgeist builds from public property records. # From free text to a parcel (/guides/parse-then-match) This guide builds one function: free text in, a decision out. It parses the line, stops early when the parse isn't a street address, looks up the parcel, and turns each lookup status into something your code can act on. **Before you start:** an API key in `FIELDGEIST_API_KEY`. See the [Quickstart](/quickstart). ## 1. Parse [#1-parse] Parse first. The lookup needs a street name and a ZIP or city, and only street addresses have parcels, so the parse tells you whether a lookup is worth paying for. ## 2. Check the parse [#2-check-the-parse] - `type` must be `street`. PO boxes, rural routes and military addresses have no parcel; a lookup would return `422 unsupported_type`. - The parse needs `components.streetName`, and `components.zip5` or `components.city`. Without them a lookup returns `400`. - Warnings in the "Out of scope" group (`unsupported_intersection`, `possibly_foreign`, `empty_input`) mean the line isn't one US delivery address. ## 3. Look up [#3-look-up] Send the parse response as is. The lookup reads its `type` and `components`. ## 4. Act on the status [#4-act-on-the-status] | Check | Then | | --- | --- | | `unit_required` is `true` | Ask for the unit, or keep the `building_id`. Check this first: it can come with `matched`. | | `status` is `matched` | Store `fieldgeist_id`. If `fuzzy` is `true`, the street name was corrected; consider confirming. | | `status` is `ambiguous` | Show the `candidates`, or ask for the missing suffix or direction. | | `status` is `no_situs_parcel` | Weak match. Review before using. | | `status` is `matched_building` | The building matched, not the unit you named. | | `status` is `not_found` | Check the input. | ## The whole thing [#the-whole-thing] ```js const API = "https://api.fieldgeist.com/v1/verify"; const headers = { Authorization: `Bearer ${process.env.FIELDGEIST_API_KEY}`, "Content-Type": "application/json", }; async function call(path, body) { const res = await fetch(`${API}${path}`, { method: "POST", headers, body: JSON.stringify(body), }); const json = await res.json(); if (!res.ok) { const id = res.headers.get("x-request-id"); throw new Error(`${res.status} ${json.error} (${id ?? "no request id"})`); } return json; } const OUT_OF_SCOPE = new Set(["unsupported_intersection", "possibly_foreign", "empty_input"]); export async function resolveAddress(line) { const parsed = await call("/parse", { line }); if (parsed.type !== "street" || parsed.warnings.some((w) => OUT_OF_SCOPE.has(w.code))) { return { kind: "not_a_street_address", parsed }; } const c = parsed.components; if (!c.streetName || !(c.zip5 || c.city)) { return { kind: "incomplete", parsed }; } const match = await call("/lookup", parsed); if (match.unit_required) { return { kind: "needs_unit", buildingId: match.building_id, unitCount: match.unit_count, match }; } switch (match.status) { case "matched": return { kind: "matched", fieldgeistId: match.fieldgeist_id, fuzzy: match.fuzzy, match }; case "ambiguous": return { kind: "ambiguous", candidates: match.candidates }; case "not_found": return { kind: "not_found" }; default: // matched_building, no_situs_parcel: usable, but worth a look. return { kind: "review", match }; } } ``` ```python import os import requests API = "https://api.fieldgeist.com/v1/verify" HEADERS = {"Authorization": f"Bearer {os.environ['FIELDGEIST_API_KEY']}"} OUT_OF_SCOPE = {"unsupported_intersection", "possibly_foreign", "empty_input"} def call(path, body): res = requests.post(f"{API}{path}", headers=HEADERS, json=body, timeout=10) data = res.json() if not res.ok: request_id = res.headers.get("x-request-id", "no request id") raise RuntimeError(f"{res.status_code} {data.get('error')} ({request_id})") return data def resolve_address(line): parsed = call("/parse", {"line": line}) if parsed["type"] != "street" or any(w["code"] in OUT_OF_SCOPE for w in parsed["warnings"]): return {"kind": "not_a_street_address", "parsed": parsed} c = parsed["components"] if not c.get("streetName") or not (c.get("zip5") or c.get("city")): return {"kind": "incomplete", "parsed": parsed} match = call("/lookup", parsed) if match.get("unit_required"): return { "kind": "needs_unit", "building_id": match["building_id"], "unit_count": match["unit_count"], "match": match, } status = match["status"] if status == "matched": return {"kind": "matched", "fieldgeist_id": match["fieldgeist_id"], "fuzzy": match["fuzzy"]} if status == "ambiguous": return {"kind": "ambiguous", "candidates": match["candidates"]} if status == "not_found": return {"kind": "not_found"} # matched_building, no_situs_parcel: usable, but worth a look. return {"kind": "review", "match": match} ``` ## Cost [#cost] Each address costs a parse and a lookup. Checking the parse before the lookup saves the lookup, the larger of the two, for lines that can't match. Later, refresh a stored parcel with [Look up by ID](/api-reference/lookup-by-id), which costs less than a new lookup. Rates are on the [pricing page](https://fieldgeist.com/pricing). ## Next steps [#next-steps] - [Condos and units](/concepts/condos-and-units): what `unit_required` means and how to ask for a unit. - [Confidence and warnings](/concepts/confidence-and-warnings): when to send an answer for review. - [Errors](/api-reference/errors): what to retry. # Introduction (/) Fieldgeist is an AI lab building tools for real estate. **Verify**, its first tool, is an address model: **US-LEX-1**, a purpose-built neural model trained on real US addresses, that reads free text the way USPS Publication 28 defines an address. Behind it sits a parcel index Fieldgeist builds, so every reading can be matched to a property. The API is how you use both today. How it reads an address, its versions, and how it improves. Verify does two jobs: - **Parse.** Send one line of free text, get back the address split into USPS Publication 28 components (house number, directionals, street, suffix, unit, city, state, ZIP), the standardized delivery lines, a confidence score and warnings for anything missing or unclear. The model reads every line, at the edge, inside the request. - **Look up.** Send a parsed address, get back the parcel it belongs to: a stable Fieldgeist ID, the county APN, the county FIPS code and coordinates. When the address could be more than one parcel, or needs a unit number to pick one, the answer says so instead of guessing. ```bash curl https://api.fieldgeist.com/v1/verify/parse \ -H "Authorization: Bearer $FIELDGEIST_API_KEY" \ -H "Content-Type: application/json" \ -d '{"line": "350 5th ave apt 21b new york ny 10118"}' ``` ```json { "input": "350 5th ave apt 21b new york ny 10118", "type": "street", "components": { "city": "NEW YORK", "addressNumber": "350", "streetSuffix": "AVE", "streetName": "5TH", "occupancyType": "APT", "occupancyId": "21B", "state": "NY", "zip5": "10118" }, "lines": ["350 5TH AVE APT 21B", "NEW YORK NY 10118"], "warnings": [], "confidence": 0.9999020747368791, "model": { "name": "us-lex-1", "version": "0.1.0", "backend": "model" } } ``` ## Who it's for [#who-its-for] - **Proptech teams** that need clean addresses and a reliable link from an address to a property record. - **AI agent builders** whose agents call address and property tools and need typed answers with explicit uncertainty (`ambiguous`, `unit_required`) rather than a confident guess. ## What it covers [#what-it-covers] | | Parse | Look up | Look up by ID | | --- | --- | --- | --- | | Endpoint | `POST /v1/verify/parse` | `POST /v1/verify/lookup` | `GET /v1/verify/lookup/{fieldgeist_id}` | | Input | One line of text | Address components (or a parse response) | A Fieldgeist ID (`adr_…`) | | Output | Components, delivery lines, confidence, warnings | Match status, parcel IDs, APN, FIPS, coordinates, candidates | One parcel and its address | | Scope | US addresses: street, PO box, rural route, military, general delivery | Street addresses | Any ID a lookup returned | The parser standardizes what you send; it does not correct spelling. Near-miss street names are handled by the lookup, which flags them with `fuzzy: true` and a lower confidence. ## Start here [#start-here] Create a key and parse your first address. API keys, the Authorization header and key hygiene. Statuses, candidates and units. Prepaid credits, billed per answer. # Quickstart (/quickstart) This page takes you from a new account to a parsed address and a parcel match. You need an email address and a terminal. ### Create an account [#create-an-account] Go to [app.fieldgeist.com/sign-up](https://app.fieldgeist.com/sign-up) and enter your email. We email you a 6-digit code; enter it to sign in. There are no passwords. The first time you sign in, the portal asks for your name and a team name. A team owns its API keys, usage and billing. Every new account starts with free credits. ### Create an API key [#create-an-api-key] In the portal, open **API keys** and select **Create key**. Name it after where it will run, such as "Production server". Copy the key when it appears. It starts with `fg_live_` and **is shown only once**. Store it in your secrets manager, then put it in an environment variable for this walkthrough: ```bash export FIELDGEIST_API_KEY="fg_live_..." ``` ### Parse an address [#parse-an-address] ```bash curl https://api.fieldgeist.com/v1/verify/parse \ -H "Authorization: Bearer $FIELDGEIST_API_KEY" \ -H "Content-Type: application/json" \ -d '{"line": "400 Broad St, Seattle, WA 98109"}' ``` ```js const res = await fetch("https://api.fieldgeist.com/v1/verify/parse", { method: "POST", headers: { Authorization: `Bearer ${process.env.FIELDGEIST_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ line: "400 Broad St, Seattle, WA 98109", }), }); const parsed = await res.json(); console.log(parsed.components); ``` ```python import os import requests res = requests.post( "https://api.fieldgeist.com/v1/verify/parse", headers={"Authorization": f"Bearer {os.environ['FIELDGEIST_API_KEY']}"}, json={"line": "400 Broad St, Seattle, WA 98109"}, timeout=10, ) parsed = res.json() print(parsed["components"]) ``` The response splits the line into USPS Publication 28 components and renders the standardized delivery lines: ```json { "input": "400 Broad St, Seattle, WA 98109", "type": "street", "components": { "city": "SEATTLE", "addressNumber": "400", "streetSuffix": "ST", "streetName": "BROAD", "state": "WA", "zip5": "98109" }, "lines": ["400 BROAD ST", "SEATTLE WA 98109"], "warnings": [], "confidence": 0.9999376787822885, "model": { "name": "us-lex-1", "version": "0.1.0", "backend": "model" } } ``` An empty `warnings` array and a confidence close to 1 mean the parse is clean. See [Confidence and warnings](/concepts/confidence-and-warnings) for what to do when they aren't. ### Match it to a parcel [#match-it-to-a-parcel] Send the parse response, unchanged, to the lookup endpoint: ```bash curl https://api.fieldgeist.com/v1/verify/parse \ -H "Authorization: Bearer $FIELDGEIST_API_KEY" \ -H "Content-Type: application/json" \ -d '{"line": "400 Broad St, Seattle, WA 98109"}' \ | curl https://api.fieldgeist.com/v1/verify/lookup \ -H "Authorization: Bearer $FIELDGEIST_API_KEY" \ -H "Content-Type: application/json" \ -d @- ``` ```js const match = await fetch("https://api.fieldgeist.com/v1/verify/lookup", { method: "POST", headers: { Authorization: `Bearer ${process.env.FIELDGEIST_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(parsed), }).then((r) => r.json()); console.log(match.status, match.fieldgeist_id); ``` ```python match = requests.post( "https://api.fieldgeist.com/v1/verify/lookup", headers={"Authorization": f"Bearer {os.environ['FIELDGEIST_API_KEY']}"}, json=parsed, timeout=10, ).json() print(match["status"], match["fieldgeist_id"]) ``` A `matched` answer carries one parcel: ```json { "status": "matched", "fieldgeist_id": "adr_01JA2Z3Y4X5W6V7T8S9R0QPNMA", "building_id": null, "apn": "198520-0495", "fips": "53033", "lat": 47.620494, "lon": -122.349235, "matchConfidence": 1, "candidates": [], "searched": ["matchkey/v3.2/zip5=98109/part.parquet"], "readPath": "chunk", "fuzzy": false } ``` Parcel values in these docs are illustrative. Run the request to see the real parcel. Store the `fieldgeist_id`. It's how you refer to this parcel later, and you can fetch it again with [Look up by Fieldgeist ID](/api-reference/lookup-by-id) at a lower price than a new lookup. ## Next steps [#next-steps] - [Handle every lookup status](/guides/parse-then-match): `ambiguous`, `unit_required` and `not_found` included. - [Errors](/api-reference/errors): every error code the API returns. - [Credits and billing](/billing): when a request is billed and how to control spend. - Try addresses without writing code in the portal's **Verify** page. Its requests are billed like API requests. # Rate limits and quotas (/rate-limits) This page lists every limit the API enforces today. If a limit isn't here, the API doesn't enforce it. ## Request limits [#request-limits] | Limit | Value | When reached | | --- | --- | --- | | Key checks that miss our key cache | 20 per minute per IP address | `429 rate_limited` for the rest of the minute. A key is cached after its first check (a valid key for 10 minutes, an unknown one for 5), so in practice only sending many different unknown keys reaches this. Malformed keys are rejected with `401` before they count. | | Parse input length | 256 characters per `line` | `413 line_too_long` | | Lookup candidates | 10 per answer | The list is cut at 10. For buildings, `unit_count` still gives the full number of units. | There is no per-key or per-team request rate limit on valid keys. ## Spending limits [#spending-limits] Spending is controlled by the team, in the portal's **Billing** page. | Limit | Set by | When reached | | --- | --- | --- | | Credit balance | Credits you add (and the sign-up credits) | `402 insufficient_credit` until credits are added. | | Monthly budget | Owners and Admins, optional | `402 budget_exceeded` until the budget is raised or removed. Counts spend per calendar month (UTC). | | Spend alert | Owners and Admins, optional, below the budget | An email when the month's spend reaches it. Requests continue. | See [Credits and billing](/billing). ## Account limits [#account-limits] | Limit | Value | | --- | --- | | Active API keys per team | 25 | | API key name | 64 characters | | Credit top-up | $5 to $5,000 at a time | ## Data retention [#data-retention] | Data | Kept for | | --- | --- | | Request logs (the portal's **Logs**) | 30 days | | Hourly usage totals (the portal's **Usage**) | 13 months | Request logs don't store IP addresses. See the [Privacy Policy](https://fieldgeist.com/privacy).