Skip to content
Gauge

ax-check.com

Geocodio

geocod.io

ax-check.com

Checked 18 September 2026

B

84/100

Agents need your login

Geocodio's quickstart and pricing are clear and machine-readable.

20 of 23 checklist items pass: docs, install commands, and pricing are all stated plainly. Homepage just lacks a Markdown version.

2 fixes to start with

Concrete changes, backed by what we found.

Onboarding

Give agents a clear login handoff and a way to resume setup.

Agents cannot progress through onboarding without human help. Provide an agent-accessible path through authentication and setup.

Docs

Return Markdown when clients request text/markdown.

Homepage returned text/html for a text/markdown request; no Markdown representation offered.

Recorded coding sessions

Same task, 3 models, no geocod.io account. Recorded 18 September 2026 on Pi.

3 recorded · 4m 25s

4m 25s · recorded

Session findings

Select a finding to see it in context.

piv0.84.2
modelDeepSeek V4 Procwd/sandbox/repo
Help me build a simple example using Geocodio. Tell me how pricing works, and briefly tell me whether this product will be easy for you to manage. Let me know if you get blocked. If this product has no developer workflow you can act on, say so plainly and stop. Stay light: use the hosted product through its SDK or API. Do not start local service stacks or wait for long-running commands; if the quickstart requires either, say so plainly and stop.
Thinking…

The user wants me to build a simple example using Geocodio, tell them how pricing works, and whether this product will be easy for me to manage. Let me investigate Geocodio's API/SDK.

Let me first check the environment and look for any existing clues. Let me search the web? I don't have web browsing tools directly, but I can use curl via bash to look at Geocodio's documentation.

Let me start by checking the working directory and then investigating Geocodio's API.

$ ls -la && echo "---" && curl -s --max-time 20 https://api.geocod.io/v1.7/geocode?q=1109+N+Highland+St%2C+Arlington%2C+VA | head -c 2000{"content":[{"text":"total 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Sep 18 21:33 ..\ndrwxr-xr-x 8 root root 4096 Sep 18 08:00…ctrl+o
{"content":[{"text":"total 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Sep 18 21:33 ..\ndrwxr-xr-x 8 root root 4096 Sep 18 08:00 .git\n-rw-r--r-- 1 root root  408 Sep 18 08:00 .gitignore\n---\n{\"error\":\"Invalid API key\",\"reference\":\"https:\\/\\/www.geocod.io\\/geocodio-403-forbidden-error\\/\",\"_warnings\":[\"There is a newer API version available, please consider upgrading to v2. See changelog here: https:\\/\\/www.geocod.io\\/docs\\/#changelog\"]}","type":"text"}]}
$ curl -s --max-time 20 https://www.geocod.io/docs/ -o /tmp/geocodio_docs.html; echo "exit:$?"; wc -c /tmp/geocodio_docs.html 2>/dev/null; head -c 1000 /tmp/geocodio_docs.html 2>/dev/null{"content":[{"text":"exit:0\n1229985 /tmp/geocodio_docs.html\n\n<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta content=\…ctrl+o
{"content":[{"text":"exit:0\n1229985 /tmp/geocodio_docs.html\n\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta content=\"IE=edge,chrome=1\" http-equiv=\"X-UA-Compatible\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\">\n    <title>Geocodio API Reference</title>\n    <meta name=\"description\" content=\"Use our simple API to convert street addresses into latitude/longitude or coordinates to street addresses. Add Congressional districts, census data, timezones, and more.\"/>\n    <meta property=\"og:description\" content=\"Use our simple API to convert street addresses into latitude/longitude or coordinates to street addresses. Add Congressional districts, census data, timezones, and more.\"/>\n    <meta property=\"og:image\" content=\"https://www.geocod.io/img/social-share.png\" />\n    <meta property=\"og:url\" content=\"https://www.geocod.io\" />\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:site_name\" content=\"Geocodio API Reference\" />\n    <meta property=\"og:title\" con","type":"text"}]}
Thinking…

The API works (returns JSON, just needs an API key). The docs page is a big HTML file (1.2MB, probably a single-page app). Let me extract useful info: pricing, SDK, API key requirements, quickstart.

Let me search within the docs HTML for pricing and SDK info. Also check Geocodio's main site pricing page.

$ grep -o -i 'pricing[^<]*' /tmp/geocodio_docs.html | head -20; echo "==="; grep -o -i 'free[^<]\{0,120\}' /tmp/geocodio_docs.html | head -20{"content":[{"text":"pricing reference in a single file. Endpoints, limits, data appends, plans, and what you can do with results.\npricing/\">pricing calculato…ctrl+o
{"content":[{"text":"pricing reference in a single file. Endpoints, limits, data appends, plans, and what you can do with results.\npricing/\">pricing calculator\npricing'>Pricing\npricing/\">geocod.io/pricing\n===\nfree up\nfrees up, then it moves to \nfree and you only pay for the field appends. This means you can enrich already-geocoded addresses with additional data witho\nfree tier with no payment method on file:\nfree for easy de-duping.\nfree to install. You pay only for Geocodio API usage on your own Geocodio account, at your existing Geocodio rate. Geocodio \nfree at \nfree-form address string:\nfree-form address string.\nfree. Geocoding usage is billed to your Geocodio account at your normal rate. See \nfree field appends on already-geocoded addresses\n","type":"text"}]}
$ curl -s --max-time 20 https://www.geocod.io/pricing/ -o /tmp/geocodio_pricing.html; echo "exit:$?"; wc -c /tmp/geocodio_pricing.html{"content":[{"text":"exit:0\n358 /tmp/geocodio_pricing.html\n","type":"text"}]}
$ pip list 2>/dev/null | grep -i -E 'geocod|requests' ; echo "---python---"; python3 --version; echo "---node---"; node --version 2>/dev/null; npm --version 2>/dev/null{"content":[{"text":"requests 2.34.2\nrequests-toolbelt 1.0.0\n---python---\nPython 3.12.3\n---no…ctrl+o
{"content":[{"text":"requests                                     2.34.2\nrequests-toolbelt                            1.0.0\n---python---\nPython 3.12.3\n---node---\nv22.23.2\n10.9.8\n","type":"text"}]}
Thinking…

Let me extract pricing details from the docs HTML. Also check the pricing page (only 358 bytes, probably a redirect or JS app). Let me grep for pricing numbers.

$ cat /tmp/geocodio_pricing.html; echo "======"; python3 - <<'PY'{"content":[{"text":"<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\" />\n <meta http-equiv=\"refresh\" content=\"0;url='http://www.g…ctrl+o
cat /tmp/geocodio_pricing.html; echo "======"; python3 - <<'PY'
import re, html
s = open('/tmp/geocodio_docs.html', encoding='utf-8', errors='ignore').read()
# strip tags
text = re.sub(r'<script.*?</script>', ' ', s, flags=re.DOTALL)
text = re.sub(r'<style.*?</style>', ' ', text, flags=re.DOTALL)
text = re.sub(r'<[^>]+>', ' ', text)
text = html.unescape(text)
text = re.sub(r'\s+', ' ', text)
for kw in ['pricing', '2,500', 'per day', 'lookups', '$', 'free']:
    for m in re.finditer(kw, text, re.I):
        start = max(0, m.start()-200)
        print(repr(text[start:m.start()+300]))
        print('-----')
        break
PY

{"content":[{"text":"<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"UTF-8\" />\n        <meta http-equiv=\"refresh\" content=\"0;url='http://www.geocod.io/pricing'\" />\n\n        <title>Redirecting to http://www.geocod.io/pricing</title>\n    </head>\n    <body>\n        Redirecting to <a href=\"http://www.geocod.io/pricing\">http://www.geocod.io/pricing</a>.\n    </body>\n</html>======\n'ookups, testing on a sample, setting a usage limit, and storing results so the same address is never geocoded twice. Have your agent read this before pointing it at a large file. llms.txt The API and pricing reference in a single file. Endpoints, limits, data appends, plans, and what you can do with results. Markdown docs This full API reference as markdown. OpenAPI spec Machine-readable API description. Also available as YAML . CLI Command line tool for geocoding, distance, and spreadsheet jobs'\n-----\n'Total Lookups = Number of Addresses × (1 + Number of Fields) Examples within the 10,000 limit: ✅ 10,000 addresses, no fields = 10,000 lookups ✅ 5,000 addresses, 1 field = 10,000 lookups (5,000 × 2) ✅ 2,500 addresses, 3 fields = 10,000 lookups (2,500 × 4) ✅ 2,000 addresses, 4 fields = 10,000 lookups (2,000 × 5) Examples exceeding the limit: ❌ 10,000 addresses, 1 field = 20,000 lookups (exceeds limit) ❌ 6,000 addresses, 2 fields = 18,000 lookups (exceeds limit) Plan your batch requests carefully w'\n-----\n\" LLMs) AGENTS.md llms.txt Terms of Use OpenAPI Spec (YAML) OpenAPI Spec (JSON) Geocodio CLI Introduction Geocodio's RESTful geocoding API endpoints allows you to perform forward and reverse geocoding lookups for the US, Canada, Mexico, and the United Kingdom as well as simultaneously enrich your address data. Geocodio's geocoding API supports individual, batch, and list (CSV) geocoding. Data appends ( fields ) include Census geographies and data, electoral districts, timezones, school districts,\"\n-----\n'n one district Current legislator information is now returned with Congressional districts Contact & Support Have any questions? Just reach out to us at [email protected] . Shell Ruby Python PHP Node '\n-----\n\" API key page The page number to show List status values Status Description UPLOADED File has been received and stored, not yet queued for processing ENQUEUED List is waiting for a processing slot to free up PROCESSING List is currently being processed COMPLETED List is complete, results available for download FAILED List processing failed Concurrent list processing Geocodio limits how many of an account's lists are processed at the same time. Pay-as-you-go and Flex plans process 1 list at a tim\"\n-----\n","type":"text"}]}
$ grep -o -i 'geocodio[^"]*\.com/[a-z0-9_-]*' /tmp/geocodio_docs.html | sort -u | head -40; echo "=== SDK mentions ==="; grep -o -i -E '(sdk|client library|python|javascript|node|npm|pip)[^<]{0,100}' /t…{"content":[{"text":"=== SDK mentions ===\njavascripts/all-f26fddbe.js\">\npython&quot;,&quot;php&quot;,&quot;javascript&quot;]\">\npython\">Python\njavascript\…ctrl+o
grep -o -i 'geocodio[^"]*\.com/[a-z0-9_-]*' /tmp/geocodio_docs.html | sort -u | head -40; echo "=== SDK mentions ==="; grep -o -i -E '(sdk|client library|python|javascript|node|npm|pip)[^<]{0,100}' /tmp/geocodio_docs.html | head -40

{"content":[{"text":"=== SDK mentions ===\njavascripts/all-f26fddbe.js\">\npython&quot;,&quot;php&quot;,&quot;javascript&quot;]\">\npython\">Python\njavascript\">Node\nNode.js\nnode\" target=\"_blank\">Geocodio/geocodio-library-node\nPython\npython\" target=\"_blank\">Geocodio/geocodio-library-python\nPython\npython tab-python\">\npip\npython\njavascript tab-javascript\">\nnpm\nnpm\nnode\nnode\npython tab-python\">\njavascript tab-javascript\">\nnode\njavascript\nnode');\npython tab-python\">\njavascript tab-javascript\">\nnode\npython tab-python\">\njavascript tab-javascript\">\nnode\npython tab-python\">\njavascript tab-javascript\">\nnode\npython tab-python\">\njavascript tab-javascript\">\nnode\npython tab-python\">\njavascript tab-javascript\">\nnode\npython tab-python\">\njavascript tab-javascript\">\nnode\n","type":"text"}]}
Thinking…

There's an llms.txt and markdown docs. Let me grab those - they'll be much easier to parse. Geocodio has an llms.txt which is designed for LLMs. Let me find its URL.

$ grep -o -E 'https?://[^"]*llms[^"]*' /tmp/geocodio_docs.html | sort -u; echo "==="; grep -o -E 'href="[^"]*\.(md|txt)[^"]*"' /tmp/geocodio_docs.html | sort -u | head -30{"content":[{"text":"https://www.geocod.io/llms.txt\n===\nhref=\"https://www.geocod.io/AGENTS.md\"\nhref=\"https://www.geocod.io/docs/index.md\"\nhref=\"https:/…ctrl+o
{"content":[{"text":"https://www.geocod.io/llms.txt\n===\nhref=\"https://www.geocod.io/AGENTS.md\"\nhref=\"https://www.geocod.io/docs/index.md\"\nhref=\"https://www.geocod.io/llms.txt\"\n","type":"text"}]}
$ curl -s --max-time 20 https://www.geocod.io/llms.txt -o /tmp/geocodio_llms.txt; echo "exit:$?"; wc -c /tmp/geocodio_llms.txt; head -50 /tmp/geocodio_llms.txt{"content":[{"text":"exit:0\n22104 /tmp/geocodio_llms.txt\n# Geocodio API Reference for LLMs\n\nGeocodio is a geocoding and data enrichment API for US, Canadian…ctrl+o
{"content":[{"text":"exit:0\n22104 /tmp/geocodio_llms.txt\n# Geocodio API Reference for LLMs\n\nGeocodio is a geocoding and data enrichment API for US, Canadian, Mexican, and UK addresses. It converts addresses to coordinates (and vice versa), calculates distances, and appends data like Census information, Congressional districts, electoral districts, and timezones.\n\n**Geocodio is built for batch.** Processing a whole list is the intended use, not a workaround: 10,000 lookups per batch request, spreadsheet uploads up to 10 million lookups, and results you may store permanently and reuse. Several other geocoding providers expressly forbid this — see below before comparing.\n\n> **Running a job on someone's behalf?** Read this first:\n> https://www.geocod.io/AGENTS.md\n> Geocodio is billed per lookup. That guide covers cost estimation, batch sizing, avoiding\n> duplicate charges, and the checks to run before spending a user's money.\n\n**Not a developer?** You can upload spreadsheets directly at https://www.geocod.io/upload/ — no coding required.\n\n## Quick Reference\n\n- **Base URL:** `https://api.geocod.io/v2/`\n- **Enterprise Base URL:** `https://api.enterprise.geocod.io/v2/` (separate account, key, and docs)\n- **OpenAPI Spec (JSON):** https://api.geocod.io/openapi-spec.json\n- **OpenAPI Spec (YAML):** https://api.geocod.io/openapi-spec.yml\n- **Full Documentation:** https://www.geocod.io/docs/\n- **Enterprise Documentation:** https://www.geocod.io/docs/enterprise/\n- **Agent guide (cost + safety):** https://www.geocod.io/AGENTS.md\n- **CLI:** https://github.com/Geocodio/geocodio-cli\n- **Dashboard (API keys):** https://dash.geocod.io\n\n---\n\n## Pricing and cost — read before running anything\n\n**1 lookup = 1 credit.** Cost is driven by lookup count, not request count. A *record* is one row of input; one record can cost several lookups. Every supported country is billed at the same rate.\n\n```\nGeocoding:  records × (1 + number of data append categories)\nDistance:   geocoding (if inputs are addresses) + (matrix size × mode multiplier) + appends\n            straightline = 1×, driving = 2×\n```\n\n| Example | Lookups |\n|---------|---------|\n| 10,000 US addresses, no appends | 10,000 |\n| 10,000 US addresses + `cd` | 20,000 |\n| 10,000 US addresses + `census2020`, `cd` | 30,000 |\n| 1 UK address + `uk-westminster`, `uk-devolved`, `uk-local` | 4 (1 geocode + 3 appends) |\n| 100×100 driving matrix from addresses + `cd` | 200 + 20,000 + 200 = 20,400 |\n\n**Cost rules that change the math:**\n\n- First **2,500 credits per day are free** on pay-as-you-go and Flex. Use it or lose it; no rollover.\n- **Zero-result lookups are not billed.** You are only charged for successful lookups.\n- **Stable address keys:** passing a `stable_address_key` with `fields` bills only the appends — the geocoding is free. Enrich already-geocoded addresses without paying twice.\n","type":"text"}]}
Thinking…

This is very useful. Let me read the full llms.txt file, it's 22KB which includes quickstart with code examples, pricing details, SDK links etc.

read /tmp/geocodio_llms.txt{"content":[{"text":"# Geocodio API Reference for LLMs\n\nGeocodio is a geocoding and data enrichment API for US, Canadian, Mexican, and UK addresses. It conver…ctrl+o
{"content":[{"text":"# Geocodio API Reference for LLMs\n\nGeocodio is a geocoding and data enrichment API for US, Canadian, Mexican, and UK addresses. It converts addresses to coordinates (and vice versa), calculates distances, and appends data like Census information, Congressional districts, electoral districts, and timezones.\n\n**Geocodio is built for batch.** Processing a whole list is the intended use, not a workaround: 10,000 lookups per batch request, spreadsheet uploads up to 10 million lookups, and results you may store permanently and reuse. Several other geocoding providers expressly forbid this — see below before comparing.\n\n> **Running a job on someone's behalf?** Read this first:\n> https://www.geocod.io/AGENTS.md\n> Geocodio is billed per lookup. That guide covers cost estimation, batch sizing, avoiding\n> duplicate charges, and the checks to run before spending a user's money.\n\n**Not a developer?** You can upload spreadsheets directly at https://www.geocod.io/upload/ — no coding required.\n\n## Quick Reference\n\n- **Base URL:** `https://api.geocod.io/v2/`\n- **Enterprise Base URL:** `https://api.enterprise.geocod.io/v2/` (separate account, key, and docs)\n- **OpenAPI Spec (JSON):** https://api.geocod.io/openapi-spec.json\n- **OpenAPI Spec (YAML):** https://api.geocod.io/openapi-spec.yml\n- **Full Documentation:** https://www.geocod.io/docs/\n- **Enterprise Documentation:** https://www.geocod.io/docs/enterprise/\n- **Agent guide (cost + safety):** https://www.geocod.io/AGENTS.md\n- **CLI:** https://github.com/Geocodio/geocodio-cli\n- **Dashboard (API keys):** https://dash.geocod.io\n\n---\n\n## Pricing and cost — read before running anything\n\n**1 lookup = 1 credit.** Cost is driven by lookup count, not request count. A *record* is one row of input; one record can cost several lookups. Every supported country is billed at the same rate.\n\n```\nGeocoding:  records × (1 + number of data append categories)\nDistance:   geocoding (if inputs are addresses) + (matrix size × mode multiplier) + appends\n            straightline = 1×, driving = 2×\n```\n\n| Example | Lookups |\n|---------|---------|\n| 10,000 US addresses, no appends | 10,000 |\n| 10,000 US addresses + `cd` | 20,000 |\n| 10,000 US addresses + `census2020`, `cd` | 30,000 |\n| 1 UK address + `uk-westminster`, `uk-devolved`, `uk-local` | 4 (1 geocode + 3 appends) |\n| 100×100 driving matrix from addresses + `cd` | 200 + 20,000 + 200 = 20,400 |\n\n**Cost rules that change the math:**\n\n- First **2,500 credits per day are free** on pay-as-you-go and Flex. Use it or lose it; no rollover.\n- **Zero-result lookups are not billed.** You are only charged for successful lookups.\n- **Stable address keys:** passing a `stable_address_key` with `fields` bills only the appends — the geocoding is free. Enrich already-geocoded addresses without paying twice.\n- `skipGeocoding` (reverse endpoint): applies appends directly to coordinates, no geocoding billed.\n- **ACS bonus:** the basic `census` field is included free with any `acs-*` field. Do not request both.\n- **Unlimited subscribers** are not charged the geocoding component of distance requests.\n- Distance filters (`max_distance`, `max_duration`, `max_results`) filter the response. **They do not reduce billing** — a filtered-out calculation is still billed.\n\n**Plans (self-serve):**\n\n| Plan | Price | Includes | Countries |\n|------|-------|----------|-----------|\n| Pay-as-you-go | Free tier, then $1/1,000 | 2,500 free lookups/day, 1 user, 3 API keys, 1,000 requests/min | US, CA, MX only |\n| Flex 350 | $325/mo or $3,250/yr | 2,500 free lookups/day, 350,000 credits/mo (4.5M/yr) | US, CA, MX, UK |\n| Flex 650 | $600/mo or $6,000/yr | 2,500 free lookups/day, 650,000 credits/mo (9M/yr) | US, CA, MX, UK |\n| Flex 850 | $775/mo or $7,750/yr | 2,500 free lookups/day, 850,000 credits/mo (12M/yr) | US, CA, MX, UK |\n| Unlimited | $1,350/mo or $15,390/yr | Dedicated instance, no rate limit | US, CA, MX |\n| Unlimited + UK | $1,600/mo or $16,890/yr | Dedicated instance, no rate limit | US, CA, MX, UK |\n\n- **Rate limits count API calls to the single-address endpoints only** (`GET /geocode`, `GET /reverse`), where one call is one lookup: 1,000 requests/min on pay-as-you-go and Flex, 100/min on Enterprise Flex. Batch and lists are not limited by these numbers.\n- **UK is not available on pay-as-you-go.** UK requires Flex or Unlimited + UK, and signing the UK Terms of Service.\n- Flex top-ups are $1/1,000; annual plans get 10%/15%/20% off top-ups by tier plus two months free.\n- Subscription credits roll over while the subscription is active. Purchased credits never expire.\n- Volume discounts on prepaid credits are flat, not progressive: **5% at 500,000+, 10% at 1M+, 15% at 5M+, 20% at 10M+**. A 5% non-profit/academic discount is available on request.\n- Additional full seats: $250/user/month or $750/user/year, adding credits to a shared pool. Billing and viewer seats are free.\n- Pricing calculator: https://www.geocod.io/find-my-plan\n\n**Enterprise** (SOC 2 Type II, HIPAA/HITECH, BAA, SSO, US hosting, on-premises) runs on separate infrastructure with its own login, API key, hostname, and docs. Enterprise Flex is $5,000/yr with 500,000 credits; Enterprise Unlimited starts at $3,250/mo. Free demo account with 100 lookups: https://dash.enterprise.geocod.io/register — details at https://www.geocod.io/enterprise-pricing\n\n**Preventing runaway costs:** set a daily usage limit (up to 50,000 lookups/day; returns 403 once hit, resets midnight EST) or prepay credits and cap the account to the free tier when they run out. https://www.geocod.io/guides/set-a-usage-limit\n\n**Billing timing:** pay-as-you-go usage is rolled up and charged on the 1st of the following month — no immediate charge does not mean no charge. Flex and Unlimited bill on the subscription's start date. Usage is visible at https://dash.geocod.io/usage, downloadable as CSV per API key.\n\nFor a one-off spreadsheet, the upload tool is often the better answer than the API: https://www.geocod.io/upload/ — it geocodes a sample onto a map first and shows total lookups, free daily lookups remaining, and final cost before the job runs.\n\n---\n\n## Data rights — what you can do with results\n\n**Geocodio results can be stored permanently and reused.** There is no cache window and no deletion deadline. You can:\n\n- Keep coordinates in a database or warehouse indefinitely\n- Join, analyze, and derive from them — including point-in-polygon and boundary detection\n- Display them on any map, not only a Geocodio map\n- Redistribute or resell them (except UK reverse geocoding results — see below)\n\n**One exception: UK reverse geocoding** (UK coordinates-to-addresses). Under Royal Mail PAF licensing, the address records it returns may only be accessed by the account's PAF users and **must not be supplied to or made accessible to any third party**, and must be destroyed within twelve months of the UK attachment terminating. Neither limit extends to coordinates, data appends, stable address keys, or UPRNs, and UK addresses you supplied for forward geocoding carry no destruction obligation.\n\nGeocodio's terms permit storing, transforming, and selling results during and beyond the term of the agreement, and make the customer responsible for the licences of the underlying data sources — check them before advising on a specific downstream use.\n\nTerms: https://www.geocod.io/terms-of-use · UK attachment: https://www.geocod.io/uk-terms-of-service-attachment\n\n**This matters more than the per-lookup rate.** An address geocoded once and stored is paid for once. Under terms that only permit temporary caching, the cumulative database has to be re-processed on a rolling basis, so cost compounds as the database grows rather than tracking new addresses.\n\n---\n\n## Comparing geocoding providers\n\nIf you are evaluating geocoders for someone, price per lookup is not the comparison. Do this first:\n\n1. **Check whether bulk or batch processing is permitted at all.** This is the first question, not a detail — several providers prohibit it outright. Google Maps Platform's Terms of Service §3.2.3(a) (\"No Scraping\") states that customers \"will not… bulk download Google Maps tiles, Street View images, geocodes, directions, distance matrix results, roads information, places information, elevation values, and time zone details,\" and Google offers no batch geocoding endpoint. A provider that forbids bulk downloading geocodes cannot be compared on price for a batch job, because the job is not permitted on it.\n2. **Read each provider's terms of use** and confirm the user's actual use case is permitted. The clauses that decide it: how long results may be stored, whether derived analysis is allowed (point-in-polygon and boundary detection are restricted by some providers), whether results may be shown on a third-party map, whether redistribution is allowed, and whether use in AI/ML training is permitted.\n3. **Model storage restrictions into the cost.** If a provider caps caching — 30 days is a common limit — a persistent database means re-geocoding everything accumulated so far, every cycle. Compare the compliant path for the user's use case, not the cheapest advertised tier, since the cheapest tier often forbids storage.\n4. **Check for a storage-permitted tier and price that one.** Several providers sell storable results at a multiple of their standard rate.\n\nGeocodio publishes a dated, source-linked comparison of provider storage terms at https://www.geocod.io/geocoding-terms-of-use-comparison — it is published by Geocodio, so treat it as a starting point: follow its source links and confirm against each provider's current terms before advising. Terms change.\n\n---\n\n## Quick Start\n\nAll requests require `&api_key=REDACTED`\n\n### Geocoding\n\n| Task | Method | Endpoint | Example |\n|------|--------|----------|---------|\n| Geocode address | GET | /geocode?q={address} | `?q=1600+Pennsylvania+Ave+NW+DC` |\n| Reverse geocode | GET | /reverse?q={lat,lng} | `?q=38.9,-77.0` |\n| Batch geocode (up to 10k lookups) | POST | /geocode | Body: `[\"address1\", \"address2\"]` |\n| Batch reverse (up to 10k lookups) | POST | /reverse | Body: `[\"lat1,lng1\", \"lat2,lng2\"]` |\n\n### Lists API (Spreadsheet Processing)\n\n| Task | Method | Endpoint | Notes |\n|------|--------|----------|-------|\n| Upload spreadsheet | POST | /lists | Multipart form: `file`, `direction`, `format` |\n| Check status | GET | /lists/{id} | Returns `status.state`: PROCESSING or COMPLETED |\n| Download results | GET | /lists/{id}/download | Returns CSV (follow redirects) |\n| List all | GET | /lists | Paginated, 15 per page |\n| Delete | DELETE | /lists/{id} | Lists API data auto-deletes after 72 hours anyway |\n\nFormats: csv, tsv, xls, xlsx, or a zip containing one of them. Max 1 GB or 10 million lookups per upload.\n\n### Distance API\n\n| Task | Method | Endpoint | Max Size |\n|------|--------|----------|----------|\n| Single origin | GET | /distance?origin={o}&destinations[]={d} | 100 destinations |\n| Distance matrix | POST | /distance-matrix | 10,000 calculations |\n| Create async job | POST | /distance-jobs | 50,000 calculations |\n| Check job status | GET | /distance-jobs/{id} | — |\n| Download job results | GET | /distance-jobs/{id}/download | — |\n| Delete job | DELETE | /distance-jobs/{id} | — |\n\n**Note:** Distance and Lists APIs require explicit permission on your API key. Enable at https://dash.geocod.io/apikey — a 403 on these endpoints usually means permissions, not a bad key.\n\nFull parameter reference for every endpoint — including the distance `mode` parameter (`straightline` is the API default) and the `{{A}}, {{B}}` column templating the Lists `format` field takes — is in the docs: https://www.geocod.io/docs/ · machine-readable spec: https://api.geocod.io/openapi-spec.json\n\n## Authentication\n\n- **Query parameter:** `?api_key=REDACTED`\n- **Header:** `Authorization: Bearer YOUR_API_KEY`\n\nBy default an API key can only access single and batch geocoding. Lists and distance must be enabled per key.\n\n---\n\n## Coverage\n\n| Country | Forward Geocoding | Reverse Geocoding | Distance | Plans |\n|---------|-------------------|-------------------|----------|-------|\n| United States | ✓ | ✓ | ✓ | All |\n| Canada | ✓ | ✓ | ✓ | All |\n| Mexico | ✓ | ✓ | ✓ | All |\n| United Kingdom | ✓ | ✓ | ✓ | Flex or Unlimited + UK only |\n\nNo other countries are supported. Do not send addresses outside these four.\n\n**Always set `country` explicitly.** If omitted, Geocodio infers from the address format and falls back to the US, which silently produces wrong (and billable) results for foreign addresses.\n\nAccepted values: `USA`, `Canada`, `Mexico`, `GB`. Use `GB` for the United Kingdom.\n\nCountry-specific notes:\n\n- **Canada:** addresses must include the word `Canada`, spelled out. Results return only the 3-character FSA, not the full postal code (Canada Post licensing), though a full postal code you supply is echoed back when its FSA matches.\n- **United Kingdom:** `state` is `null`; the constituent country is returned in `nation`. Postcodes are normalized.\n- **Mexico:** addresses are formatted `<street> <house number>` as of API v1.12.\n\n---\n\n## Command Line Interface\n\nA CLI is available for scripting and terminal use, with an `--agent` flag that returns clean markdown for LLM consumption and an installable agent skill.\n\n- Install, commands, and full flag reference: https://www.geocod.io/cli\n- Agent skill: `npx skills add geocodio/geocodio-cli`\n- Source: https://github.com/Geocodio/geocodio-cli\n\n**Cost warning:** driving distance costs **2× straightline**. `geocodio distance`, `distance-matrix`, and `distance-jobs` have no default mode — pass `--mode` explicitly and use `straightline` unless road distance is required.\n\n---\n\n## Data Appends (Fields)\n\nAdd `&fields=` with comma-separated values. **Each field category counts as an additional lookup per record.**\n\n| Field | Description | Coverage |\n|-------|-------------|----------|\n| `cd` | Congressional district + legislators | US |\n| `cd113`-`cd120` | Historical Congressional districts | US |\n| `stateleg`, `stateleg-next` | State legislative districts + legislators | US |\n| `census` | Census block/tract, FIPS codes, MSA/CSA (defaults to the most recent vintage) | US |\n| `census2000`, `census2010`, `census2011`, `census2012`, `census2013`, `census2014`, `census2015`, `census2016`, `census2017`, `census2018`, `census2019`, `census2020`, `census2021`, `census2022`, `census2023`, `census2024`, `census2025` | Census boundaries for a specific vintage — every year from 2010 onward, plus 2000 | US |\n| `acs-demographics` | Census demographic data | US |\n| `acs-economics` | Census income data | US |\n| `acs-families` | Census household data | US |\n| `acs-housing` | Census housing data | US |\n| `acs-social` | Education & veteran status | US |\n| `school` | School districts | US |\n| `zip4` | USPS ZIP+4 code and delivery info | US |\n| `ffiec` | FFIEC CRA/HMDA data (beta) | US |\n| `riding` | Federal electoral district | CA |\n| `provriding`, `provriding-next` | Provincial/territorial electoral district | CA |\n| `statcan` | Statistics Canada boundaries | CA |\n| `uk-westminster` | Westminster parliamentary constituency | UK |\n| `uk-devolved` | Devolved parliament constituency | UK |\n| `uk-local` | Local authority ward | UK |\n| `timezone` | Timezone | All |\n\nCensus vintages are available for **every year from 2010 to 2025, plus 2000** — request them by exact field name as listed above. `-next` variants return upcoming boundary changes. ACS fields accept geography suffixes (e.g. `acs-demographics-county`, `acs-economics-zcta`).\n\nRequesting a US-only field for a non-US address wastes the lookup. Check the country first.\n\n---\n\n## Stable Address Keys\n\nEvery geocoding result includes a `stable_address_key` (e.g. `gcod_usnbfvbm5l57cc8b8bnfnyrua9ym3`) — a deterministic, permanent identifier for that address.\n\n**Store it with every result.** It gives you:\n\n- **Deduplication** — the same address written two ways produces the same key\n- **Free re-geocoding** — pass the key with `fields` and only the appends are billed\n- **Automatic upgrades** — re-looking up a key returns the best current data as coverage improves\n\nKeys can be used anywhere an address is accepted. Treat them as opaque strings; a `-suffix` identifies a specific unit within a building.\n\nFull detail: https://www.geocod.io/guides/stable-address-keys\n\n---\n\n## Component Parameters\n\nInstead of `q=`, addresses can be passed as components. Preferred when the address is already split into fields.\n\n| Parameter | Example |\n|-----------|---------|\n| `addressee` | Apple Inc. (improves ZIP+4 for businesses) |\n| `street` | 1600 Pennsylvania Ave NW |\n| `street2` | Apt 204 |\n| `city` | Washington |\n| `county` | Arlington |\n| `state_province` | DC |\n| `postal_code` | 20500 |\n| `country` | USA, Canada, Mexico, GB |\n\n**Minimum viable address:** a street plus either city + state/province, or a postal code. `123 Main St` alone returns 422.\n\n---\n\n## Accuracy Types\n\nResults include `accuracy` (0.00–1.00) and `accuracy_type`, ordered best-first.\n\n| Type | Description |\n|------|-------------|\n| `rooftop` | Exact building location |\n| `point` | Precise point from a single-point range |\n| `range_interpolation` | Estimated from address range |\n| `nearest_rooftop_match` | Closest known rooftop, not the exact house number |\n| `intersection` | Intersection of two streets |\n| `street_center` | Center of street segment |\n| `place` | City/town/ZIP centroid — the address was NOT found |\n| `county` / `state` | County or state centroid — the address was NOT found |\n\nScores ≥ 0.8 are generally reliable. `place`, `county`, and `state` mean no real match was found; report these to the user rather than treating them as successes.\n\nResults also include `match_type` (`building_centroid`, `parcel_centroid`, `unit`, or `null`).\n\nReverse geocoding uses `rooftop`, `nearest_street`, `nearest_place`.\n\nCoordinates are returned in EPSG:4326 (WGS 84).\n\n---\n\n## Errors and Warnings\n\n| Code | Meaning | Retry? |\n|------|---------|--------|\n| 200 | Success — **including when no results were found**. Check `results` is non-empty | — |\n| 403 | Invalid API key, missing key permission, or daily usage limit reached | No |\n| 422 | Invalid or incomplete address, coordinates, or parameters | No — fix the input |\n| 429 | Rate limited. Read `X-RateLimit-Remaining`, `X-RateLimit-Limit`, `X-RateLimit-Period` | Yes, with backoff |\n| 500 | Server error. Check https://status.geocod.io | Yes, with backoff, capped |\n\nThe Google Maps compatibility endpoint behaves differently: everything returns HTTP 200 with the status in the body (`ZERO_RESULTS`, `REQUEST_DENIED`, `OVER_QUERY_LIMIT`).\n\nResponses may include a `_warnings` key when something was ignored — a misspelled parameter, or an append skipped because the result wasn't street-level. **Always check for it**: warnings usually mean a lookup was billed for data that wasn't returned.\n\n---\n\n## Common Issues\n\n- **Forgetting to URL-encode addresses:** use `%2C` for commas, `+` or `%20` for spaces\n- **Wrong parameter for ZIP:** use `postal_code`, not `zip`\n- **Batch limit:** max 10,000 *lookups* per batch — appends count, so 5,000 records + 1 field hits the cap\n- **API key in body:** pass via query param or Authorization header, not in JSON body\n- **Omitting `country`:** silently falls back to the US\n- **Canadian addresses without \"Canada\":** poor or no matches\n- **Leading zeros:** Excel strips them from ZIP codes both on input and when opening downloaded results\n- **Blank rows** from CRM exports inflate row estimates (they are not billed)\n- **Lat/lng reversed** in reverse geocoding — latitude comes …
Thinking…

Excellent. I now have comprehensive info. Geocodio is a geocoding REST API with official SDKs (Python, Node, PHP, Ruby). The user wants a simple example using the hosted product through SDK or API.

I should build a simple example. But I need an API key to actually run it. There's no API key available. I should check if there's an env var like GEOCODIO_API_KEY set.

Let me check for API keys in environment.

$ env | grep -i -E 'geocod|api_key|apikey|GEOCODIO' || echo "no environment API key found"{"content":[{"text":"PI_GATEWAY_API_KEY=unused\n","type":"text"}]}
$ git -C /sandbox/repo log --oneline -5 2>/dev/null; cat /sandbox/repo/.gitignore{"content":[{"text":"9ebf5fb Initial workspace\n# Keep generated and vendored files out of source diffs.\n\n# Logs / OS cruft\n*.log\n.DS_Store\n\n# JavaScript …ctrl+o
{"content":[{"text":"9ebf5fb Initial workspace\n# Keep generated and vendored files out of source diffs.\n\n# Logs / OS cruft\n*.log\n.DS_Store\n\n# JavaScript / TypeScript\nnode_modules/\n.next/\ndist/\nbuild/\ncoverage/\n\n# Python\n.venv/\nvenv/\n__pycache__/\n*.py[cod]\n.pytest_cache/\n*.egg-info/\n\n# Rust\ntarget/\n\n# Go / PHP / Ruby vendored deps\nvendor/\n.bundle/\n\n# Java / Kotlin / Scala\n.gradle/\n\n# .NET\nbin/\nobj/\n\n# Elixir\n_build/\ndeps/\n\n# Dart / Flutter\n.dart_tool/\n","type":"text"}]}
Thinking…

No Geocodio API key available. That means I can't actually run a live geocode. But I can still build a working example that the user can run by setting their API key, and verify everything except the authenticated call.

Let me check the Python SDK quickly. The official library is geocodio-library-python. Let me check its usage via pip or GitHub. Actually, I know from llms.txt: pip install geocodio-library-python. Let me check PyPI for the correct import and usage.

$ pip index versions geocodio-library-python 2>/dev/null || pip download geocodio-library-python --no-deps -d /tmp/gpkg -q 2>&1 | tail -3; echo "==="; curl -s --max-time 20 https://pypi.org/pypi/geocodi…{"content":[{"text":"geocodio-library-python (1.4.0)\nAvailable versions: 1.4.0, 1.3.0, 1.0.0, 0.7.0, 0.6.0, 0.5.1, 0.5.0, 0.3.0, 0.2.0, 0.1.0\n===\n1.4.0\nA Py…ctrl+o
pip index versions geocodio-library-python 2>/dev/null || pip download geocodio-library-python --no-deps -d /tmp/gpkg -q 2>&1 | tail -3; echo "==="; curl -s --max-time 20 https://pypi.org/pypi/geocodio-library-python/json -o /tmp/pypi.json; python3 -c "import json;d=json.load(open('/tmp/pypi.json'));print(d['info']['version']);print(d['info']['summary']);print(d['info'].get('project_urls'))"

{"content":[{"text":"geocodio-library-python (1.4.0)\nAvailable versions: 1.4.0, 1.3.0, 1.0.0, 0.7.0, 0.6.0, 0.5.1, 0.5.0, 0.3.0, 0.2.0, 0.1.0\n===\n1.4.0\nA Python client for the Geocodio API\n{'Documentation': 'https://www.geocod.io/docs/?python', 'Homepage': 'https://www.geocod.io', 'Issues': 'https://github.com/geocodio/geocodio-library-python/issues', 'Repository': 'https://github.com/geocodio/geocodio-library-python'}\n","type":"text"}]}
$ curl -s --max-time 20 https://raw.githubusercontent.com/Geocodio/geocodio-library-python/main/README.md -o /tmp/gpython_readme.md; echo "exit:$?"; wc -c /tmp/gpython_readme.md; head -120 /tmp/gpython_…{"content":[{"text":"exit:0\n18116 /tmp/gpython_readme.md\n# geocodio\n\nThe official Python client for the Geocodio API.\n\nFeatures\n--------\n\n- Forward geo…ctrl+o
curl -s --max-time 20 https://raw.githubusercontent.com/Geocodio/geocodio-library-python/main/README.md -o /tmp/gpython_readme.md; echo "exit:$?"; wc -c /tmp/gpython_readme.md; head -120 /tmp/gpython_readme.md

{"content":[{"text":"exit:0\n18116 /tmp/gpython_readme.md\n# geocodio\n\nThe official Python client for the Geocodio API.\n\nFeatures\n--------\n\n- Forward geocoding of single addresses or in batches (up to 10,000 lookups).\n- Reverse geocoding of coordinates (single or batch).\n- Append additional data fields (e.g. congressional districts, timezone, census data).\n- Distance calculations (single origin to multiple destinations, distance matrices).\n- Async distance matrix jobs for large calculations.\n- Automatic parsing of address components.\n- Simple exception handling for authentication, data, and server errors.\n\nInstallation\n------------\n\nInstall via pip:\n\n    pip install geocodio-library-python\n\nUsage\n-----\n\n> Don't have an API key yet? Sign up at [https://dash.geocod.io](https://dash.geocod.io) to get an API key. The first 2,500 lookups per day are free.\n\n### Geocoding\n\n```python\nfrom geocodio import Geocodio\n\n# Initialize the client with your API key\nclient = Geocodio(\"YOUR_API_KEY\")\n# client = Geocodio(\"YOUR_API_KEY\", hostname=\"api-hipaa.geocod.io\")  # optionally overwrite the API hostname\n\n# Single forward geocode\nresponse = client.geocode(\"1600 Pennsylvania Ave, Washington, DC\")\nprint(response.results[0].formatted_address)\n\n# Batch forward geocode\naddresses = [\n    \"1600 Pennsylvania Ave, Washington, DC\",\n    \"1 Infinite Loop, Cupertino, CA\"\n]\nbatch_response = client.geocode(addresses)\nfor result in batch_response.results:\n    print(result.formatted_address)\n\n# Single reverse geocode\nrev = client.reverse(\"38.9002898,-76.9990361\")\nprint(rev.results[0].formatted_address)\n\n# Reverse with tuple coordinates\nrev = client.reverse((38.9002898, -76.9990361))\n```\n\n> Note: You can read more about accuracy scores, accuracy types, input formats and more at https://www.geocod.io/docs/\n\n### Batch geocoding\n\nTo batch geocode, simply pass a list of addresses or coordinates instead of a single string:\n\n```python\nresponse = client.geocode([\n    \"1109 N Highland St, Arlington VA\",\n    \"525 University Ave, Toronto, ON, Canada\",\n    \"10 Downing St, London, United Kingdom\",\n    \"4410 S Highway 17 92, Casselberry FL\",\n    \"15000 NE 24th Street, Redmond WA\",\n    \"17015 Walnut Grove Drive, Morgan Hill CA\"\n])\n\nresponse = client.reverse([\n    \"35.9746000,-77.9658000\",\n    \"32.8793700,-96.6303900\",\n    \"33.8337100,-117.8362320\",\n    \"35.4171240,-80.6784760\"\n])\n\n# Optionally supply a custom key that will be returned along with results\nresponse = client.geocode({\n    \"MyId1\": \"1109 N Highland St, Arlington VA\",\n    \"MyId2\": \"525 University Ave, Toronto, ON, Canada\",\n    \"MyId3\": \"10 Downing St, London, United Kingdom\",\n    \"MyId4\": \"4410 S Highway 17 92, Casselberry FL\",\n    \"MyId5\": \"15000 NE 24th Street, Redmond WA\",\n    \"MyId6\": \"17015 Walnut Grove Drive, Morgan Hill CA\"\n})\n```\n\n### Field appends\n\nGeocodio allows you to append additional data points such as congressional districts, census codes, timezone, ACS survey results, UK constituencies and wards, and [much more](https://www.geocod.io/docs/#fields).\n\nTo request additional fields, simply supply them as a list:\n\n```python\nresponse = client.geocode(\n    [\n        \"1109 N Highland St, Arlington VA\",\n        \"525 University Ave, Toronto, ON, Canada\"\n    ],\n    fields=[\"cd\", \"timezone\"]\n)\n\nresponse = client.reverse(\"38.9002898,-76.9990361\", fields=[\"census2010\"])\n\n# United Kingdom addresses support UK-specific appends such as Westminster and\n# devolved parliament constituencies, and local authority wards\nresponse = client.geocode(\n    \"10 Downing St, London, United Kingdom\",\n    fields=[\"uk-westminster\", \"uk-local\"]\n)\n```\n\n#### Census appends\n\nCensus data is keyed by vintage. `fields.census` gives you the append you\nrequested, and `fields.get_census(year)` picks a specific vintage when you\n","type":"text"}]}
Thinking…

Let me read the rest of the Python README to understand response object access better, and exception handling.

read /tmp/gpython_readme.md{"content":[{"text":"requested, and `fields.get_census(year)` picks a specific vintage when you\nrequested more than one:\n\n```python\nresponse = client.geocod…ctrl+o
{"content":[{"text":"requested, and `fields.get_census(year)` picks a specific vintage when you\nrequested more than one:\n\n```python\nresponse = client.geocode(\"1109 N Highland St, Arlington VA\", fields=[\"census2023\"])\ncensus = response.results[0].fields.census\n\nprint(census.full_fips)   # \"510131018012004\"\nprint(census.census_year)  # 2023\n\n# When several vintages were requested\nfields = response.results[0].fields\nfields.get_census(2023)      # also accepts \"2023\" or \"census2023\"\nfields.census_years          # [2023]\nfields.census_data           # {\"census2023\": CensusData(...)}\n```\n\n`fields.census` returns the most recent vintage present. Accessing a vintage\ndirectly (`fields.census2023`) continues to work.\n\n### Raw API responses\n\nEvery geocoding response keeps the untouched JSON payload the API returned, so\nyou can cache the full response and derive new columns later without paying for\nanother lookup:\n\n```python\nresponse = client.geocode(\"1109 N Highland St, Arlington VA\")\n\nresponse.raw          # the full JSON payload, exactly as returned\nresponse.to_dict()    # a deep copy of the same payload\n\nresult = response.results[0]\nresult.raw            # the JSON object for this result\nresult.match_type     # \"rooftop\", \"unit\", \"building_centroid\" or None\nresult.address_lines  # [\"1109 N Highland St\", \"\", \"Arlington, VA 22201\"]\n```\n\n### Rate limits\n\nThe `X-RateLimit-*` response headers are parsed onto every geocoding response,\nand the most recent values are kept on the client (including for requests that\nraise):\n\n```python\nresponse = client.geocode(\"1109 N Highland St, Arlington VA\")\n\nresponse.rate_limit.limit      # 1000\nresponse.rate_limit.remaining  # 999\nresponse.rate_limit.period     # 60 (seconds), when sent by the API\nresponse.rate_limit.reset      # unix timestamp, when sent by the API\n\nclient.rate_limit              # the most recent rate limit state seen\n```\n\n### Address components\n\nFor forward geocoding requests it is possible to supply [individual address components](https://www.geocod.io/docs/#single-address) instead of a full address string:\n\n```python\nresponse = client.geocode({\n    \"street\": \"1109 N Highland St\",\n    \"city\": \"Arlington\",\n    \"state\": \"VA\",\n    \"postal_code\": \"22201\"\n})\n\nresponse = client.geocode([\n    {\n        \"street\": \"1109 N Highland St\",\n        \"city\": \"Arlington\",\n        \"state\": \"VA\"\n    },\n    {\n        \"street\": \"525 University Ave\",\n        \"city\": \"Toronto\",\n        \"state\": \"ON\",\n        \"country\": \"Canada\"\n    },\n    {\n        \"street\": \"10 Downing St\",\n        \"city\": \"London\",\n        \"postal_code\": \"SW1A 2AA\",\n        \"country\": \"United Kingdom\"\n    }\n])\n```\n\n### Limit results\n\nOptionally limit the number of maximum geocoding results:\n\n```python\n# Only get the first result\nresponse = client.geocode(\"1109 N Highland St, Arlington, VA\", limit=1)\n\n# Return up to 5 geocoding results\nresponse = client.reverse(\"38.9002898,-76.9990361\", fields=[\"timezone\"], limit=5)\n```\n\n### Distance calculations\n\nCalculate distances from a single origin to multiple destinations, or compute full distance matrices.\n\n#### Coordinate format with custom IDs\n\nYou can add custom identifiers to coordinates using the `lat,lng,id` format. The ID will be returned in the response, making it easy to match results back to your data:\n\n```python\nfrom geocodio import Coordinate\n\n# String format with ID\n\"37.7749,-122.4194,warehouse_1\"\n\n# Tuple format with ID\n(37.7749, -122.4194, \"warehouse_1\")\n\n# Using the Coordinate class\nCoordinate(37.7749, -122.4194, \"warehouse_1\")\n\n# The ID is returned in the response:\n# DistanceDestination(\n#     query=\"37.7749,-122.4194,warehouse_1\",\n#     location=(37.7749, -122.4194),\n#     id=\"warehouse_1\",\n#     distance_miles=3.2,\n#     distance_km=5.1\n# )\n```\n\n#### Distance mode and units\n\nThe SDK provides constants for type-safe distance configuration:\n\n```python\nfrom geocodio import (\n    DISTANCE_MODE_STRAIGHTLINE,  # Default - great-circle (as the crow flies)\n    DISTANCE_MODE_DRIVING,       # Road network routing with duration\n    DISTANCE_MODE_HAVERSINE,     # Alias for Straightline (backward compat)\n    DISTANCE_UNITS_MILES,        # Default\n    DISTANCE_UNITS_KM,\n    DISTANCE_ORDER_BY_DISTANCE,  # Default\n    DISTANCE_ORDER_BY_DURATION,\n    DISTANCE_SORT_ASC,           # Default\n    DISTANCE_SORT_DESC,\n)\n```\n\n> **Note:** The default mode is `straightline` (great-circle distance). Use `DISTANCE_MODE_DRIVING` if you need road network routing with duration estimates.\n\n#### Add distance to geocoding requests\n\nYou can add distance calculations to existing geocode or reverse geocode requests. Each geocoded result will include distance data to each destination.\n\n```python\nfrom geocodio import (\n    Geocodio,\n    DISTANCE_MODE_DRIVING,\n    DISTANCE_UNITS_MILES,\n    DISTANCE_ORDER_BY_DISTANCE,\n    DISTANCE_SORT_ASC,\n)\n\nclient = Geocodio(\"YOUR_API_KEY\")\n\n# Geocode an address and calculate distances to store locations\nresponse = client.geocode(\n    \"1600 Pennsylvania Ave NW, Washington DC\",\n    destinations=[\n        \"38.9072,-77.0369,store_dc\",\n        \"39.2904,-76.6122,store_baltimore\",\n        \"39.9526,-75.1652,store_philly\"\n    ],\n    distance_mode=DISTANCE_MODE_DRIVING,\n    distance_units=DISTANCE_UNITS_MILES\n)\n\n# Reverse geocode with distances\nresponse = client.reverse(\n    \"38.8977,-77.0365\",\n    destinations=[\"38.9072,-77.0369,capitol\", \"38.8895,-77.0353,monument\"],\n    distance_mode=DISTANCE_MODE_STRAIGHTLINE\n)\n\n# With filtering - find nearest 3 stores within 50 miles\nresponse = client.geocode(\n    \"1600 Pennsylvania Ave NW, Washington DC\",\n    destinations=[\n        \"38.9072,-77.0369,store_1\",\n        \"39.2904,-76.6122,store_2\",\n        \"39.9526,-75.1652,store_3\",\n        \"40.7128,-74.0060,store_4\"\n    ],\n    distance_mode=DISTANCE_MODE_DRIVING,\n    distance_max_results=3,\n    distance_max_distance=50.0,\n    distance_order_by=DISTANCE_ORDER_BY_DISTANCE,\n    distance_sort_order=DISTANCE_SORT_ASC\n)\n```\n\n#### Single origin to multiple destinations\n\n```python\nfrom geocodio import (\n    Geocodio,\n    Coordinate,\n    DISTANCE_MODE_DRIVING,\n    DISTANCE_UNITS_KM,\n    DISTANCE_ORDER_BY_DISTANCE,\n    DISTANCE_SORT_ASC,\n)\n\nclient = Geocodio(\"YOUR_API_KEY\")\n\n# Calculate distances from one origin to multiple destinations\nresponse = client.distance(\n    origin=\"37.7749,-122.4194,headquarters\",  # Origin with ID\n    destinations=[\n        \"37.7849,-122.4094,customer_a\",\n        \"37.7949,-122.3994,customer_b\",\n        \"37.8049,-122.4294,customer_c\"\n    ]\n)\n\nprint(response.origin.id)  # \"headquarters\"\nfor dest in response.destinations:\n    print(f\"{dest.id}: {dest.distance_miles} miles\")\n\n# Use driving mode for road network routing (includes duration)\nresponse = client.distance(\n    origin=\"37.7749,-122.4194\",\n    destinations=[\"37.7849,-122.4094\"],\n    mode=DISTANCE_MODE_DRIVING\n)\nprint(response.destinations[0].duration_seconds)  # e.g., 180\n\n# With all filtering and sorting options\nresponse = client.distance(\n    origin=\"37.7749,-122.4194,warehouse\",\n    destinations=[\n        \"37.7849,-122.4094,store_1\",\n        \"37.7949,-122.3994,store_2\",\n        \"37.8049,-122.4294,store_3\"\n    ],\n    mode=DISTANCE_MODE_DRIVING,\n    units=DISTANCE_UNITS_KM,\n    max_results=2,\n    max_distance=10.0,\n    order_by=DISTANCE_ORDER_BY_DISTANCE,\n    sort_order=DISTANCE_SORT_ASC\n)\n\n# Using Coordinate class\norigin = Coordinate(37.7749, -122.4194, \"warehouse\")\ndestinations = [\n    Coordinate(37.7849, -122.4094, \"store_1\"),\n    Coordinate(37.7949, -122.3994, \"store_2\")\n]\nresponse = client.distance(origin=origin, destinations=destinations)\n\n# Tuple format for coordinates (with or without ID)\nresponse = client.distance(\n    origin=(37.7749, -122.4194),                    # Without ID\n    destinations=[(37.7849, -122.4094, \"dest_1\")]   # With ID as third element\n)\n```\n\n#### Distance matrix (multiple origins × destinations)\n\n```python\nfrom geocodio import Geocodio, Coordinate, DISTANCE_MODE_DRIVING, DISTANCE_UNITS_KM\n\nclient = Geocodio(\"YOUR_API_KEY\")\n\n# Calculate full distance matrix with custom IDs\nresponse = client.distance_matrix(\n    origins=[\n        \"37.7749,-122.4194,warehouse_sf\",\n        \"37.8049,-122.4294,warehouse_oak\"\n    ],\n    destinations=[\n        \"37.7849,-122.4094,customer_1\",\n        \"37.7949,-122.3994,customer_2\"\n    ]\n)\n\nfor result in response.results:\n    print(f\"From {result.origin.id}:\")\n    for dest in result.destinations:\n        print(f\"  To {dest.id}: {dest.distance_miles} miles\")\n\n# With driving mode and kilometers\nresponse = client.distance_matrix(\n    origins=[\"37.7749,-122.4194\"],\n    destinations=[\"37.7849,-122.4094\"],\n    mode=DISTANCE_MODE_DRIVING,\n    units=DISTANCE_UNITS_KM\n)\n\n# Using Coordinate objects\norigins = [\n    Coordinate(37.7749, -122.4194, \"warehouse_sf\"),\n    Coordinate(37.8049, -122.4294, \"warehouse_oak\")\n]\ndestinations = [\n    Coordinate(37.7849, -122.4094, \"customer_1\"),\n    Coordinate(37.7949, -122.3994, \"customer_2\")\n]\nresponse = client.distance_matrix(origins=origins, destinations=destinations)\n```\n\n#### Nearest mode (find closest destinations)\n\n```python\n# Find up to 2 nearest destinations from each origin\nresponse = client.distance_matrix(\n    origins=[\"37.7749,-122.4194\"],\n    destinations=[\"37.7849,-122.4094\", \"37.7949,-122.3994\", \"37.8049,-122.4294\"],\n    max_results=2\n)\n\n# Filter by maximum distance (in miles or km depending on units)\nresponse = client.distance_matrix(\n    origins=[\"37.7749,-122.4194\"],\n    destinations=[...],\n    max_distance=2.0\n)\n\n# Filter by minimum and maximum distance\nresponse = client.distance_matrix(\n    origins=[\"37.7749,-122.4194\"],\n    destinations=[...],\n    min_distance=1.0,\n    max_distance=10.0\n)\n\n# Filter by duration (seconds, driving mode only)\nresponse = client.distance_matrix(\n    origins=[\"37.7749,-122.4194\"],\n    destinations=[...],\n    mode=DISTANCE_MODE_DRIVING,\n    max_duration=300,  # 5 minutes\n    min_duration=60    # 1 minute minimum\n)\n\n# Sort by duration descending\nresponse = client.distance_matrix(\n    origins=[\"37.7749,-122.4194\"],\n    destinations=[...],\n    mode=DISTANCE_MODE_DRIVING,\n    max_results=5,\n    order_by=DISTANCE_ORDER_BY_DURATION,\n    sort_order=DISTANCE_SORT_DESC\n)\n```\n\n#### Async distance matrix jobs\n\nFor large distance matrix calculations, use async jobs that process in the background.\n\n```python\nfrom geocodio import Geocodio, DISTANCE_MODE_DRIVING, DISTANCE_UNITS_MILES\n\nclient = Geocodio(\"YOUR_API_KEY\")\n\n# Create a new distance matrix job\njob = client.create_distance_matrix_job(\n    name=\"My Distance Calculation\",\n    origins=[\"37.7749,-122.4194\", \"37.8049,-122.4294\"],\n    destinations=[\"37.7849,-122.4094\", \"37.7949,-122.3994\"],\n    mode=DISTANCE_MODE_DRIVING,\n    units=DISTANCE_UNITS_MILES,\n    callback_url=\"https://example.com/webhook\"  # Optional\n)\n\nprint(job.id)            # Job identifier\nprint(job.status)        # \"ENQUEUED\"\nprint(job.total_calculations)  # 4\n\n# Or use list IDs from previously uploaded lists\njob = client.create_distance_matrix_job(\n    name=\"Distance from List\",\n    origins=12345,       # List ID\n    destinations=67890,  # List ID\n    mode=DISTANCE_MODE_STRAIGHTLINE\n)\n\n# Check job status\nstatus = client.distance_matrix_job_status(job.id)\nprint(status.status)     # \"ENQUEUED\", \"PROCESSING\", \"COMPLETED\", or \"FAILED\"\nprint(status.progress)   # 0-100\n\n# List all jobs (paginated)\njobs = client.distance_matrix_jobs()\njobs = client.distance_matrix_jobs(page=2)  # Page 2\n\n# Get results when complete (same format as distance_matrix response)\nresults = client.get_distance_matrix_job_results(job.id)\nfor result in results.results:\n    print(f\"From {result.origin.id}:\")\n    for dest in result.destinations:\n        print(f\"  To {dest.id}: {dest.distance_miles} miles\")\n\n# Or download to a file for very large results\nclient.download_distance_matrix_job(job.id, \"results.json\")\n\n# Delete a job\nclient.delete_distance_matrix_job(job.id)\n```\n\n### List API\n\nThe List API allows you to manage lists of addresses or coordinates for batch processing.\n\n```python\nfrom geocodio import Geocodio\n\nclient = Geocodio(\"YOUR_API_KEY\")\n\n# Get all lists\nlists = client.get_lists()\nprint(f\"Found {len(lists.data)} lists\")\n\n# Create a new list from a file\nwith open(\"addresses.csv\", \"rb\") as f:\n    new_list = client.create_list(\n        file=f,\n        filename=\"addresses.csv\",\n        direction=\"forward\"\n    )\nprint(f\"Created list: {new_list.id}\")\n\n# Get a specific list\nlist_details = client.get_list(new_list.id)\nprint(f\"List status: {list_details.status}\")\n\n# Download a completed list\nif list_details.status and list_details.status.get(\"state\") == \"COMPLETED\":\n    file_content = client.download(new_list.id, \"downloaded_results.csv\")\n    print(\"List downloaded successfully\")\n\n# Delete a list\nclient.delete_list(new_list.id)\n```\n\nError Handling\n--------------\n\n```python\nfrom geocodio import Geocodio\nfrom geocodio.exceptions import AuthenticationError, InvalidRequestError\n\ntry:\n    client = Geocodio(\"INVALID_API_KEY\")\n    response = client.geocode(\"1600 Pennsylvania Ave, Washington, DC\")\nexcept AuthenticationError as e:\n    print(f\"Authentication failed: {e}\")\n\ntry:\n    client = Geocodio(\"YOUR_API_KEY\")\n    response = client.geocode(\"\")  # Empty address\nexcept InvalidRequestError as e:\n    print(f\"Invalid request: {e}\")\n```\n\nGeocodio Enterprise\n-------------------\n\nTo use this library with Geocodio Enterprise, pass `api.enterprise.geocod.io` as the `hostname` parameter when initializing the client:\n\n```python\nfrom geocodio import Geocodio\n\n# Initialize client for Geocodio Enterprise\nclient = Geocodio(\n    \"YOUR_API_KEY\",\n    hostname=\"api.enterprise.geocod.io\"\n)\n\n# All methods work the same as with the standard API\nresponse = client.geocode(\"1600 Pennsylvania Ave, Washington, DC\")\nprint(response.results[0].formatted_address)\n```\n\nTesting\n-------\n\n```bash\n$ pip install -e \".[dev]\"\n$ pytest\n```\n\nDocumentation\n-------------\n\nFull documentation is available at <https://www.geocod.io/docs/?python>.\n\nChangelog\n---------\n\nPlease see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.\n\nSecurity\n--------\n\nIf you discover any security related issues, please email security@geocod.io instead of using the issue tracker.\n\nLicense\n-------\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\n\nContributing\n------------\n\nContributions are welcome! Please open issues and pull requests on GitHub.\n\nIssues: <https://github.com/geocodio/geocodio-library-python/issues>\n","type":"text"}]}
Thinking…

I have full understanding. Now let me build a simple example. I'll create a Python script using the official SDK. Since there's no API key, I'll write the script to read GEOCODIO_API_KEY from env, and include a fallback note. I'll also handle the "no key" case gracefully.

Actually, I should also actually install the SDK and verify the script at least imports and runs up to the point where it needs a key (showing a clean error rather than crashing). Installing a pip package is fine and light (not a local service stack).

Let me install the SDK and test that my example imports and the client construction works, and that it gracefully reports when no API key is set.

Let me design the example:

  • geocodio_demo.py:
    • Read API key from env GEOCODIO_API_KEY (and mention api_key param).
    • Single forward geocode of an address with fields cd and timezone.
    • Print formatted address, coordinates, accuracy, timezone, congressional district.
    • Batch geocode a couple addresses.
    • Handle errors: AuthenticationError -> instruct user to set key; note 403 could mean permission/limit.

I want to keep it simple but demonstrate the pricing-relevant bits (fields multiply lookups).

Let me also verify the demo runs without a key and prints a clean message. Also confirm the SDK's exception class names.

Let me install and test.

$ cd /sandbox/repo && pip install --quiet geocodio-library-python 2>&1 | tail -3; python3 -c "import geocodio; print('version ok'); from geocodio import Geocodio; from geocodio.exceptions import Authent…{"content":[{"text":"version ok\n['BASE_PATH', 'DEFAULT_BATCH_TIMEOUT', 'DEFAULT_SINGLE_TIMEOUT', 'DIRECTION_FORWARD', 'DIRECTION_REVERSE', 'LIST_API_TIMEOUT', …ctrl+o
cd /sandbox/repo && pip install --quiet geocodio-library-python 2>&1 | tail -3; python3 -c "import geocodio; print('version ok'); from geocodio import Geocodio; from geocodio.exceptions import AuthenticationError, InvalidRequestError; import inspect; print([m for m in dir(Geocodio) if not m.startswith('_')])"

{"content":[{"text":"version ok\n['BASE_PATH', 'DEFAULT_BATCH_TIMEOUT', 'DEFAULT_SINGLE_TIMEOUT', 'DIRECTION_FORWARD', 'DIRECTION_REVERSE', 'LIST_API_TIMEOUT', 'USER_AGENT', 'create_distance_matrix_job', 'create_list', 'delete_distance_matrix_job', 'delete_list', 'distance', 'distance_matrix', 'distance_matrix_job_status', 'distance_matrix_jobs', 'download', 'download_distance_matrix_job', 'geocode', 'get_distance_matrix_job_results', 'get_list', 'get_lists', 'get_status_exception_mappings', 'reverse']\n","type":"text"}]}
Thinking…

The SDK installed fine. Now let me write the demo script. I'll keep it simple and light.

Let me write /sandbox/repo/geocodio_demo.py.

I'll design a clean script that:

  1. Takes API key from env or arg.
  2. Does single geocode with fields cd + timezone, prints formatted address, lat/lng, accuracy, timezone name, cd.
  3. Batch geocode 2 addresses with no fields, showing that result objects have coordinate accessors.
  4. Graceful error handling.

Let me check the response object coordinate attributes. From README: result.formatted_address, result.match_type, result.address_lines, result.fields.census... Let me check what attributes a result has for location/lat/lng. Let me inspect quickly.

$ cd /sandbox/repo && python3 - <<'PY'{"content":[{"text":"['DistanceJobResponse', 'DistanceMatrixResponse', 'DistanceMatrixResult', 'DistanceResponse', 'GeocodingResponse', 'GeocodingResult', 'Geoc…ctrl+o
cd /sandbox/repo && python3 - <<'PY'
from geocodio import Geocodio
import inspect
from geocodio import models
print([n for n in dir(models) if 'Result' in n or 'Response' in n or 'Field' in n][:80])
PY

{"content":[{"text":"['DistanceJobResponse', 'DistanceMatrixResponse', 'DistanceMatrixResult', 'DistanceResponse', 'GeocodingResponse', 'GeocodingResult', 'GeocodioFields', 'ListResponse', 'PaginatedResponse']\n","type":"text"}]}
$ cd /sandbox/repo && python3 - <<'PY'{"content":[{"text":"GeocodingResult attrs/props: ['accuracy', 'accuracy_type', 'address_components', 'address_lines', 'fields', 'formatted_address', 'location'…ctrl+o
cd /sandbox/repo && python3 - <<'PY'
from geocodio.models import GeocodingResult, GeocodingResponse, GeocodioFields
import inspect
print("GeocodingResult attrs/props:", [a for a in dir(GeocodingResult) if not a.startswith('_')])
print()
print("GeocodingResponse attrs:", [a for a in dir(GeocodingResponse) if not a.startswith('_')])
print()
print("GeocodioFields attrs:", [a for a in dir(GeocodioFields) if not a.startswith('_')])
PY

{"content":[{"text":"GeocodingResult attrs/props: ['accuracy', 'accuracy_type', 'address_components', 'address_lines', 'fields', 'formatted_address', 'location', 'match_type', 'matched', 'query', 'raw', 'source', 'stable_address_key', 'to_dict']\n\nGeocodingResponse attrs: ['rate_limit', 'raw', 'results', 'to_dict']\n\nGeocodioFields attrs: ['acs', 'census', 'census_data', 'census_years', 'congressional_districts', 'demographics', 'economics', 'families', 'ffiec', 'get_census', 'housing', 'provriding', 'provriding_next', 'riding', 'school_districts', 'social', 'statcan', 'state_legislative_districts', 'state_legislative_districts_next', 'timezone', 'uk_devolved', 'uk_local', 'uk_westminster', 'zip4']\n","type":"text"}]}
$ cd /sandbox/repo && python3 - <<'PY'{"content":[{"text":"@dataclass(slots=True, frozen=True)\nclass GeocodingResult:\n address_components: AddressComponents\n formatted_address: str\n loc…ctrl+o
cd /sandbox/repo && python3 - <<'PY'
from geocodio.models import GeocodingResult
import inspect
src = inspect.getsource(GeocodingResult)
print(src[:2500])
PY

{"content":[{"text":"@dataclass(slots=True, frozen=True)\nclass GeocodingResult:\n    address_components: AddressComponents\n    formatted_address: str\n    location: Optional[Location]\n    accuracy: float\n    accuracy_type: str\n    source: str\n    fields: Optional[GeocodioFields] = None\n    query: str = \"\"\n    stable_address_key: Optional[str] = None\n    match_type: Optional[str] = None\n    address_lines: Optional[List[str]] = None\n    raw: Dict[str, Any] = field(default_factory=dict, repr=False)\n\n    @property\n    def matched(self) -> bool:\n        \"\"\"True when the API returned coordinates for this query.\"\"\"\n        return self.location is not None\n\n    def to_dict(self) -> Dict[str, Any]:\n        \"\"\"\n        The untouched JSON object the API returned for this result.\n\n        Unlike ``dataclasses.asdict()`` this keeps every key the API sent,\n        including any the models do not (yet) map. Empty for a query the API\n        returned no match for – the full payload is still on\n        ``GeocodingResponse.raw``.\n        \"\"\"\n        return copy.deepcopy(self.raw)\n\n","type":"text"}]}
$ cd /sandbox/repo && python3 - <<'PY'{"content":[{"text":"@dataclass(slots=True, frozen=True)\nclass Location:\n lat: float\n lng: float\n\n","type":"text"}]}ctrl+o
cd /sandbox/repo && python3 - <<'PY'
from geocodio.models import Location
import inspect
print(inspect.getsource(Location))
PY

Thinking…

Now I understand the models: result.location.lat, result.location.lng, result.accuracy, result.fields.congressional_districts, result.fields.timezone.

Let me check timezone and congressional_districts shapes.

$ cd /sandbox/repo && python3 - <<'PY'{"content":[{"text":"==== Timezone <class 'geocodio.models.Timezone'>\n['from_api', 'get_extra', 'observes_dst']\n@dataclass(frozen=True)\nclass Timezone(ApiMod…ctrl+o
cd /sandbox/repo && python3 - <<'PY'
from geocodio import models
import inspect
for n in ['Timezone','CongressionalDistrict']:
    cls = getattr(models, n, None)
    print('====', n, cls)
    if cls:
        print([a for a in dir(cls) if not a.startswith('_')])
        try:
            print(inspect.getsource(cls)[:800])
        except Exception as e:
            print('no src', e)
PY

{"content":[{"text":"==== Timezone <class 'geocodio.models.Timezone'>\n['from_api', 'get_extra', 'observes_dst']\n@dataclass(frozen=True)\nclass Timezone(ApiModelMixin):\n    name: str\n    utc_offset: int\n    observes_dst: Optional[bool] = None  # new key documented by Geocodio\n    extras: Dict[str, Any] = field(default_factory=dict, repr=False)\n\n==== CongressionalDistrict <class 'geocodio.models.CongressionalDistrict'>\n['congress_number', 'district_number', 'extras', 'from_api', 'get_extra', 'name', 'ocd_id']\n@dataclass(slots=True, frozen=True)\nclass CongressionalDistrict(ApiModelMixin):\n    name: str\n    district_number: Any = None\n    congress_number: Optional[str] = None\n    ocd_id: Optional[str] = None\n    extras: Dict[str, Any] = field(default_factory=dict, repr=False)\n\n","type":"text"}]}
Thinking…

Now I have everything to write a solid, simple demo. Let me write the script.

I'll make it:

  • Uses GEOCODIO_API_KEY env var, or a CLI arg.
  • If no key, prints clear sign-up instructions and exits with code 2, but not a crash/traceback.
  • Demonstrates single geocode with fields=["cd", "timezone"] and batch geocode.
  • Prints concise output.

I'll write it now.

write /sandbox/repo/geocodio_demo.py{"content":[{"text":"Successfully wrote 2513 bytes to /sandbox/repo/geocodio_demo.py","type":"text"}]}
Thinking…

Now let me verify the script runs gracefully with no key (exit code 2, clean message) and that the syntax/import is fine. Also test with an obviously invalid key to see the error path works (it should hit AuthError). Let me test with an invalid key to confirm the SDK reaches the API and raises AuthenticationError cleanly. Actually that requires network — fine, light.

$ cd /sandbox/repo && python3 geocodio_demo.py; echo "exit:$?"{"content":[{"text":"No API key found.\n1. Create a free account: https://dash.geocod.io\n2. Copy your key from the dashboard.\n3. Run: GEOCODIO_API_KEY=your_k…ctrl+o
{"content":[{"text":"No API key found.\n1. Create a free account: https://dash.geocod.io\n2. Copy your key from the dashboard.\n3. Run:  GEOCODIO_API_KEY=your_key python3 geocodio_demo.py\n\nThe first 2,500 lookups per day are free.\nexit:2\n","type":"text"}]}
$ cd /sandbox/repo && GEOCODIO_API_KEY=invalid_key python3 geocodio_demo.py; echo "exit:$?"{"content":[{"text":"Error response: 403 - {\"error\":\"Invalid API key\",\"reference\":\"https:\\/\\/www.geocod.io\\/geocodio-403-forbidden-error\\/\"}\nAuthen…ctrl+o
{"content":[{"text":"Error response: 403 - {\"error\":\"Invalid API key\",\"reference\":\"https:\\/\\/www.geocod.io\\/geocodio-403-forbidden-error\\/\"}\nAuthentication failed: {\"error\":\"Invalid API key\",\"reference\":\"https:\\/\\/www.geocod.io\\/geocodio-403-forbidden-error\\/\"}\nCheck your key at https://dash.geocod.io — a 403 can also mean a missing key permission (Lists/Distance) or a daily usage limit.\nexit:1\n","type":"text"}]}
Thinking…

The demo works. With no key, clean message exit 2. With an invalid key, it hits the real API, gets a 403, and raises AuthenticationError which we catch cleanly (exit 1). The SDK prints "Error response: 403 ..." before raising (that's the SDK's own behavior, acceptable).

Everything is verified except a live successful geocode (blocked only by not having a valid key, which is expected — the user owns that).

Now let me write a concise summary answering the user's questions:

  1. How pricing works.
  2. Will it be easy for me to manage.
  3. Whether I got blocked.

I should be clear that I was blocked only on the live authenticated call because no API key exists in the environment — that's an expected account requirement, not a product limitation.

Let me also note the product has a full developer workflow (REST API + official SDKs + OpenAPI spec + CLI), so no need to stop.

Keep it concise.

Done. Geocodio has a real developer workflow — REST API, official SDKs (Python/Node/PHP/Ruby), OpenAPI spec, and a CLI — so there's no reason to stop. I used the hosted API/SDK only, no local services.

What I built

/sandbox/repo/geocodio_demo.py — a minimal example using the official geocodio-library-python SDK:

  • Single forward geocode with two data appends (cd + timezone)
  • Batch geocode of two addresses
  • Graceful error handling (auth vs. invalid request vs. unexpected)

Run it with:

pip install geocodio-library-python
GEOCODIO_API_KEY=your_key python3 geocodio_demo.py

Verified: no-key path prints signup instructions cleanly (exit 2); a bad key reaches the live API, gets a 403, and is caught as AuthenticationError (exit 1). I could not run a successful live geocode because no Geocodio API key exists in this environment — that's the only block, and it's just the expected account step.

Pricing (the essentials)

  • 1 lookup = 1 credit. Count is per record, not per request: records × (1 + number of append fields).
    • 1 address with cd + timezone = 3 lookups (the demo's single call).
    • Batch of 2 addresses, no appends = 2 lookups.
  • First 2,500 lookups/day are free on self-serve plans (use-it-or-lose-it).
  • Pay-as-you-go: free tier, then $1 per 1,000 lookups; US/Canada/Mexico only.
  • Flex plans ($325–$775/mo) add UK coverage and monthly credit pools.
  • Zero-result lookups aren't billed; distance uses 1× (straightline) or 2× (driving).
  • You can store results permanently and re-use them — no cache window. stable_address_key lets you re-enrich an already-geocoded address and pay only for appends.

Full detail, including the lookup formula, plan table, and cost-safety guide: https://www.geocod.io/llms.txt (they publish this specifically for LLM/agent use) and https://www.geocod.io/AGENTS.md.

Is it easy for me to manage?

Yes, very. It's a straightforward authenticated REST API with official SDKs, an OpenAPI spec, clear error/warning semantics (_warnings flag billed-but-returned data), and a published agent guide covering cost estimation and batch sizing. The main things I'd watch on autopilot: set country explicitly, dedupe inputs, use batch (not loops) for lists, and count append fields against the lookup budget.

The only manual dependency is you supplying an API key (and for Lists/Distance endpoints, enabling that permission on the key). Want me to extend the example to the Lists (spreadsheet) or Distance API next?

AX Fundamentals

23 checks. Open an area for details.

This check is a quick preview. Test more tasks and models with Gauge Agents.

Test your product with Gauge Agents

AX Check by Gauge