Postal Code Area Lookup for Agent Data Enrichment
Pricing
$0.10 / 1,000 postal areas
Postal Code Area Lookup for Agent Data Enrichment
Postal-area lookup from Zippopotam.us for country and postal-code pairs. Export attributed place names, state codes and approximate place centroids as JSON; does not validate street addresses.
Enrich country/postal-code pairs with attributed postal place names, state codes and approximate place centroids from the anonymous Zippopotam.us API.
Use this actor for postal-area labels and coarse geographic enrichment. A postal code may map to several places: each source place becomes its own row. Postal codes stay strings, including leading zeros. This is separate from OSM place or business discovery and does not validate street addresses or delivery eligibility.
Input
The prefilled example is:
{"lookups": [{"country": "US", "postalCode": "90210"}],"maxItems": 1}
For a mixed-country batch:
{"lookups": [{"country": "US", "postalCode": "01001"},{"country": "DE", "postalCode": "01067"}],"maxItems": 20,"concurrency": 3,"maxRetries": 2,"timeoutSecs": 15}
| Input | Meaning |
|---|---|
lookups | Required array of 1–10,000 objects containing only country and postalCode. |
country | Two-letter country code. Outer whitespace is trimmed and country letters are uppercased. Actual coverage belongs to the source. |
postalCode | Required string of 1–20 letters, digits, spaces or hyphens. Outer whitespace is trimmed; leading zeros remain. Numbers are rejected. |
maxItems | Total output-row cap, including errors: 1–100,000, default 1,000. The Store prefill is 1. |
concurrency | Simultaneous lookups: 1–10, default 3. |
maxRetries | Extra attempts for network errors, HTTP 429 or 5xx: 0–3, default 2. |
timeoutSecs | Timeout per HTTP operation: 1–60 seconds, default 15. |
Street/address fields, residents, households, reverse-person searches, contact fields and POI/business queries are unsupported. Unknown input fields are rejected. Malformed pairs produce input_error rows; an invalid overall input fails the run with a clear message. lookupIndex identifies the original pair even when its values cannot be used.
Returned fields
| Field | Meaning |
|---|---|
countryCode, postalCode | Normalized input country and string postal code; null for an unusable input pair. |
placeName | Postal place name reported by the source. |
state, stateCode | Source state/region name and abbreviation; null when unavailable. |
latitude, longitude | Numeric approximate place centroid, or both null if the source pair is unusable. These are not rooftop coordinates or postal boundaries. |
coordinateAccuracy | approximate_place_centroid or unavailable. |
coordinateWarnings | Reasons source coordinates were omitted. |
lookupIndex | Zero-based input index. Duplicate pairs remain separate inputs. |
matchIndex | Zero-based source place index; null for a lookup-level error. |
matchedPlaceCount | Number of source places before the output cap. |
matchesTruncated | True when the cap or charge budget omitted places from this lookup. |
status | ok, coverage_missing, input_error or source_error. |
errorCode, error | Machine-readable error category and explanatory text; null on success. |
httpStatus | Source HTTP status, or null if no response/request was available. |
source, sourceUrl | Zippopotam.us and the exact country/postal-code API URL. |
sourceAttribution, sourceLicenseUrl | Attribution to Zippopotam.us/GeoNames and source database license link. |
retrievedAt | UTC timestamp for the lookup result. |
SUMMARY in the default key-value store records row totals and whether the run stopped at the output cap, charge limit or end of input. The default dataset is available through the actor's Results output link and Apify dataset export.
Real output example
This row comes from the local run saved in sample_output.json:
{"lookupIndex": 0,"countryCode": "US","postalCode": "90210","placeName": "Beverly Hills","state": "California","stateCode": "CA","latitude": 34.0901,"longitude": -118.4065,"coordinateAccuracy": "approximate_place_centroid","coordinateWarnings": [],"matchIndex": 0,"matchedPlaceCount": 1,"matchesTruncated": false,"status": "ok","errorCode": null,"error": null,"httpStatus": 200,"source": "Zippopotam.us","sourceUrl": "https://api.zippopotam.us/us/90210","sourceAttribution": "Zippopotam.us; postal reference data from GeoNames (https://www.geonames.org/).","sourceLicenseUrl": "https://opendatacommons.org/licenses/odbl/1-0/","retrievedAt": "2026-10-06T09:08:17.749118+00:00"}
Pricing proposal
The proposed postal_area event is $0.0001 per delivered ok place row, or $0.10 for 1,000 successful rows. A code with three saved places incurs three events. A successful postal place with unavailable coordinates is still billed because its place/state data was returned. Coverage gaps, invalid inputs, source errors and discarded rows do not incur this event. Duplicate successful inputs incur their own events.
See PRICING.md for the current Store comparison, the free competitor caveat and the proposed event configuration. This local build does not activate Store pricing. Apify's separate platform costs or automatic start events depend on the final account configuration.
Limits and source quality
- The actor requests only
https://api.zippopotam.us/{country}/{postalCode}without login, API key, proxy, reverse search or additional enrichment sources. - HTTP 404 means missing source coverage. It does not establish that a delivery address or postal code is invalid.
- Raise
maxItemsto preserve all source matches. Truncation is explicit. Input order and source place order are retained; a small batch may already be in flight when the row cap is reached. - Retry waits grow as 1, 2, then 4 seconds.
Retry-Aftercan extend a wait to a maximum of 30 seconds. Other HTTP errors and malformed responses become item-level errors. - Missing, nonnumeric, nonfinite or out-of-range coordinate pairs become null with a warning. Values within the numeric ranges can still be wrong; the actor cannot independently certify source accuracy. No guessed correction is made.
- In the captured DE/01067 response, all three places had source latitude
14612; their names were preserved and both coordinates were omitted. This case is included in the real response fixtures and sample. - Coverage and precision vary by country. Some countries expose postal prefixes or incomplete datasets. Consult the source coverage notes and endpoint documentation.
- Zippopotam.us is a public third-party service with data supplied as-is. Keep concurrency low; availability and source changes can affect runs.
Attribution
Data is supplied by Zippopotam.us, derived from GeoNames. The Zippopotam.us source page identifies the Open Database License and Database Contents License. Attribution and a source-license link accompany each row; retain them when reusing or exporting data. Source terms apply to derived data.
FAQ
Can this verify an address for shipping? No. It provides a postal-area reference match, not a delivery-address verdict.
Why does my country or full postal code return no match? The public source may not cover it or may expose only a prefix. A 404 is saved as coverage_missing; no alternate source is queried.
Why are there multiple rows for one code? The source can associate a code with several places. Join on lookupIndex and retain matchIndex rather than silently picking a single city.
Why are coordinates missing on an ok row? A usable place name was returned, but the source coordinate pair failed numeric/range checks. Read coordinateWarnings.
Are repeated pairs deduplicated? No. Each input retains its own index and successful output events.
Use with AI agents / MCP
For an Apify-connected agent or MCP client, supply the JSON input above to this actor and read the Results dataset. Keep postalCode a JSON string. Handle coverage_missing as an unknown coverage state, retain all place matches, and check coordinateAccuracy before using coordinates. maxItems is an output-row budget, so choose a larger value than the pair count when several places may match. This actor does not implement its own MCP server; the connection is provided by the client's Apify integration.
Local development
Use the existing virtual environment; do not install packages into it:
& C:/GritWork/earn/.venv/Scripts/python.exe evidence/run_tests.py& C:/GritWork/earn/.venv/Scripts/python.exe evidence/run_tests.py liveapify validate-schema
Put the JSON input in storage/key_value_stores/default/INPUT.json, then run:
$env:APIFY_LOCAL_STORAGE_DIR = "$PWD/storage"& C:/GritWork/earn/.venv/Scripts/python.exe -m src
The scripts in evidence/ retain the exact local-run commands, build digests, observations and test results. Real response fixtures and their capture metadata are under tests/fixtures/. Cloud builds and real billing require a separate owner-run publication/configuration step.