Every Mexican zip code, state, municipality, city and settlement — one bundled SQLite database, no API key, no rate limit.
Quick start · Hosted instance · Querying the API · MCP server · Development · Architecture
Looking up a Mexican postal code usually means scraping the official Correos de México site or wrangling a clunky Excel export. Sepomex turns that same official dataset into a clean, paginated JSON API you can query by zip code, state, city, municipality or colony — and, as of v1.0.0, exposes it to AI agents through the Model Context Protocol as well.
The whole catalog (~154k settlements) ships inside the app as a SQLite database, so there's nothing external to provision: clone, load, run.
- 🔎 Search that fits the question — query zip codes by
zip_code,state,city,colony, or any combination, accent- and case-insensitive. - 🧭 Four resources — zip codes (settlements), states, municipalities and cities, each with
showand paginatedindexendpoints. - 📄 Predictable pagination — every list response carries a
meta.paginationblock plusLink/X-Total-*headers. - 🤖 MCP server built in — the same data as agentic tools over Streamable HTTP (
/mcp) and stdio (bin/mcp). - 🗃️ Zero external dependencies — bundled SQLite dataset; no Postgres, no Redis, no API keys.
- 🌐 CORS-friendly & read-only — safe to call straight from the browser.
- 🐳 Dockerized — multi-stage build, one-command dev environment, CI that ships images to Docker Hub.
No key, no quota, no tracking. Sepomex is a read-only public API over public data. Host your own in minutes — the dataset travels with the code.
Sepomex is designed to be self-hosted — the whole catalog ships with the code, so standing up your own copy is a few commands. But if you just want to try it (or wire up a quick prototype), a free public instance runs at:
https://sepomex.kurenn.dev
curl "https://sepomex.kurenn.dev/api/v1/zip_codes?zip_code=64000"This is a best-effort community instance — a single small server, rate-limited to ~300 requests per 5 minutes per IP, with no uptime or availability guarantee. For production traffic, or anything you depend on, run your own (see Development); it's the same image this instance runs.
All endpoints live under the /api/v1 path — prefix it with your host (the
examples below use a local http://localhost:3000):
/api/v1Grab every settlement for a postal code:
curl "http://localhost:3000/api/v1/zip_codes?zip_code=64000"{
"zip_codes": [
{
"id": 1,
"d_codigo": "64000",
"d_asenta": "Centro",
"d_tipo_asenta": "Colonia",
"d_mnpio": "Monterrey",
"d_estado": "Nuevo León",
"d_cp": "64000",
"...": "..."
}
],
"meta": { "pagination": { "per_page": 15, "total_pages": 1, "total_objects": 1, "links": { "first": "…", "last": "…" } } }
}Prefer to run it yourself? Jump to Development. A health probe lives at GET /up.
Interactive docs: a Swagger UI is served at
/api-docs, and the raw OpenAPI 3 spec at/api-docs/v1/swagger.yaml.
Four resources, all under /api/v1:
| Resource | Endpoint | Notes |
|---|---|---|
| Zip codes | GET /zip_codes |
Searchable by zip_code, state, city, colony |
| States | GET /states · GET /states/:id |
GET /states/:id/municipalities for its municipalities |
| Municipalities | GET /municipalities · GET /municipalities/:id |
|
| Cities | GET /cities · GET /cities/:id |
Mix and match any of the search parameters — matching is partial and accent-insensitive:
# by postal code
curl "http://localhost:3000/api/v1/zip_codes?zip_code=67173"
# by city / municipality
curl "http://localhost:3000/api/v1/zip_codes?city=monterrey"
# by state
curl "http://localhost:3000/api/v1/zip_codes?state=nuevo%20leon"
# by colony (settlement)
curl "http://localhost:3000/api/v1/zip_codes?colony=del%20valle"
# all together
curl "http://localhost:3000/api/v1/zip_codes?state=nuevo%20leon&city=guadalupe&colony=del%20valle"Example zip code response
{
"zip_codes": [
{
"id": 1,
"d_codigo": "01000",
"d_asenta": "San Ángel",
"d_tipo_asenta": "Colonia",
"d_mnpio": "Álvaro Obregón",
"d_estado": "Ciudad de México",
"d_ciudad": "Ciudad de México",
"d_cp": "01001",
"c_estado": "09",
"c_oficina": "01001",
"c_cp": null,
"c_tipo_asenta": "09",
"c_mnpio": "010",
"id_asenta_cpcons": "0001",
"d_zona": "Urbano",
"c_cve_ciudad": "01"
}
],
"meta": {
"pagination": {
"per_page": 15,
"total_pages": 9728,
"total_objects": 145906,
"links": {
"first": "/api/v1/zip_codes?page=1",
"last": "/api/v1/zip_codes?page=9728",
"next": "/api/v1/zip_codes?page=2"
}
}
}
}curl "http://localhost:3000/api/v1/states"
curl "http://localhost:3000/api/v1/states/1"
curl "http://localhost:3000/api/v1/states/1/municipalities"
curl "http://localhost:3000/api/v1/municipalities/1"
curl "http://localhost:3000/api/v1/cities/1"Example state & municipality responses
// GET /api/v1/states/1
{ "state": { "id": 1, "name": "Ciudad de México", "cities_count": 16 } }
// GET /api/v1/states/1/municipalities
{
"municipalities": [
{ "id": 1, "name": "Álvaro Obregón", "municipality_key": "010", "zip_code": "01001", "state_id": 1 },
{ "id": 16, "name": "Xochimilco", "municipality_key": "013", "zip_code": "16001", "state_id": 1 }
]
}Every index response is paginated — 15 per page by default, up to 200 via per_page (larger values fall back to 15). Combine per_page with page:
curl "http://localhost:3000/api/v1/zip_codes?per_page=200&page=2"Each response includes a meta.pagination block:
"meta": {
"pagination": {
"per_page": 15,
"total_pages": 9728,
"total_objects": 145906,
"links": {
"first": "/api/v1/zip_codes?page=1",
"last": "/api/v1/zip_codes?page=9728",
"prev": "/api/v1/zip_codes?page=1",
"next": "/api/v1/zip_codes?page=3"
}
}
}The same numbers are also returned as Link, X-Total-Pages and X-Total-Count response headers.
Sepomex ships a Model Context Protocol server so AI agents (Claude Desktop, Claude Code, Cursor, …) can query the catalog as tools. Every tool reuses the exact models and serializers behind the REST API, so both surfaces stay in sync.
| Tool | Description | Arguments |
|---|---|---|
lookup_zip_code |
All settlements for one exact CP | zip_code (required) |
search_zip_codes |
Filter settlements | zip_code, state, city, colony, limit |
list_states |
The 32 states | — |
state_municipalities |
Municipalities of a state | state_id (required) |
search_cities |
Cities by name | query, limit |
Each tool returns a human-readable summary and structuredContent (the same fields as the REST serializers).
Streamable HTTP — mounted at /mcp, alongside the REST API:
curl -X POST http://localhost:3000/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"lookup_zip_code","arguments":{"zip_code":"64000"}}}'stdio — bin/mcp serves the protocol over stdin/stdout against the local database. Add it to your client config:
{
"mcpServers": {
"sepomex": { "command": "/absolute/path/to/sepomex/bin/mcp" }
}
}By default bin/mcp uses the development database; set RAILS_ENV=production to serve the bundled production catalog.
The dataset lives in a bundled SQLite database, so there's no external service to run. Set up locally with rbenv/ruby or with Docker.
Local (rbenv / ruby)
git clone git@github.com:IcaliaLabs/sepomex.git
cd sepomex
bundle install
bin/rails db:prepare # create the SQLite database + load the schema
bin/rake data:loadev # import the ~154k settlements from the bundled CSV
bin/rails server # http://localhost:3000Docker
docker compose run --rm development bash # shell in the dev container
bin/rails db:prepare && bin/rake data:loadev
exit
docker compose up # http://localhost:3000Running specs
bundle exec rspec # locally
docker compose run --rm tests # in the container (as CI does)The bundled dataset tracks the official SEPOMEX export. One command downloads the latest export and regenerates the CSV:
rake data:refresh # download the export + rebuild lib/sepomex_db.csv
bin/rails db:reset && rake data:loadev # rebuild the local DB from itdata:refresh performs the export site's form postback for you (via FetchSepomexExport), so there's no file to download by hand. Already have a CPdescarga.xml? Import it directly instead:
rake "data:import_xml[/path/to/CPdescarga.xml]" # regenerate lib/sepomex_db.csvBoth stream the ~67 MB XML (it never loads fully into memory) and write the pipe-delimited CSV the loader consumes. A scheduled data-refresh workflow runs data:refresh monthly and opens a PR whenever the data changes.
app/
├── controllers/
│ ├── api/v1/ # REST endpoints (zip_codes, states, municipalities, cities)
│ ├── concerns/paginatable.rb # meta.pagination + Link / X-Total-* contract
│ └── mcp_controller.rb # MCP over Streamable HTTP → POST /mcp
├── mcp/sepomex_mcp/ # MCP server + the 5 tools
├── models/ # ZipCode, State, Municipality, City, FtsZipCode
├── serializers/ # Active Model Serializers (:json adapter)
└── services/ # CSV loader + XML→CSV importer
bin/mcp # MCP over stdio
lib/sepomex_db.csv # bundled dataset (~154k rows)
Stack: Ruby 3.4 · Rails 8.1 (API-only) · SQLite · Puma · Active Model Serializers · mcp
Pushing to main runs the GitHub Actions pipeline (.github/workflows): it builds the Docker testing image, runs the spec suite, then builds and pushes the release image to Docker Hub (icalia/sepomex). The production image bakes the SQLite catalog at build time and can be deployed to any container host; set a SECRET_KEY_BASE in that environment (it isn't baked into the image).
- Optional bearer-token gating for the MCP endpoint.
- Deeper agentic tooling (specialist agents / plugins).
Notable changes are recorded in CHANGELOG.md.
Pull requests are welcome — please branch off main and open one against a separate branch. This project follows the Contributor Covenant.
Code and documentation © 2013–2026 Icalia Labs, released under the MIT License.