Docs · spec v1.0 · 8 languages

Look up a zipcode in
3 lines

No signup, no API key. Install your language’s SDK and a single function returns the zipcode data.

Supported languages · SDKs are available in 8 languages — TypeScript / Go / Python / Rust / Ruby / Dart / PHP / Swift. All SDK signatures follow spec v1.0.

Introduction

jpzip is a set of static JSON files served from https://jpzip.nadai.dev. Each language SDK is a thin wrapper around that data — it fetches, manages memory / persistent caches, and lets you go all the way offline.

Install

# JavaScript / TypeScript (Node / browser / Bun / Deno)
npm i @jpzip/jpzip

# Go (1.21+)
go get github.com/jpzip/go

# Python (3.10+)
pip install jpzip

# Rust (1.75+)
cargo add jpzip

# Ruby (3.2+)
gem install jpzip

# Dart (3.0+) / Flutter
dart pub add jpzip

# PHP (8.2+)
composer require jpzip/jpzip

# Swift (5.9+) — SwiftPM Package.swift
.package(url: "https://github.com/jpzip/swift", from: "0.1.0")

60-second quickstart

Minimal example — the singleton initialises itself, so you can call it directly. Switch languages in the SDK section below.

SDK

Use the language tabs. The hash in the URL (e.g. #go-client) jumps directly to a language.

import { lookup } from "@jpzip/jpzip";

const e = await lookup("2310017");
if (e) console.log(e.prefecture, e.city, e.towns[0].town);
// 神奈川県 横浜市中区 本町 (Kanagawa, Yokohama Naka-ku, Honcho)

npm package @jpzip/jpzip. ESM / CJS dual, fully typed, runs on Node 18+, major browsers, Bun, Deno and Cloudflare Workers.

Functional API (singleton)

When one app needs one client, the functional API is shortest. It shares a lazily-initialised JpzipClient under the hood, so the L1 in-memory cache always applies.

import {
  lookup,        // (zip: string) => Promise<ZipcodeEntry | null>
  lookupGroup,   // (prefix: 1-3 digits) => Promise<ZipcodeDict>
  lookupAll,     // () => Promise<ZipcodeDict> (parallel /g/0..9)
  preload,       // ({ scope: 'all' | '0' | '23' | '231' }) => Promise<void>
  getMeta,       // () => Promise<Meta | null>
  isValidZipcode,// (zip: string) => boolean ← format check, no fetch
  configure,     // re-apply options to the singleton
} from "@jpzip/jpzip";

// Single lookup — fetch /p/231.json once, then L1 hits
const e = await lookup("2310017");

// Group — 3-digit=1 file / 2-digit=10 in parallel / 1-digit=/g/{n}.json
const dict = await lookupGroup("23");

// Full preload — fetch /g/0..9 in parallel and warm the L1
await preload({ scope: "all" });
// Subsequent lookup() / lookupGroup() need no network

JpzipClient (explicit instance)

For tests, multi-environment setups, or independent persistent caches, instantiate new JpzipClient() yourself.

import { JpzipClient } from "@jpzip/jpzip";

const client = new JpzipClient({
  // every option is optional
  baseUrl: "https://jpzip.nadai.dev",  // default
  memoryCacheSize: 100,               // number of prefixes in the L1 LRU
  cache: undefined,                  // L2 (below)
  fetch: globalThis.fetch,           // swappable in tests
  onSpecMismatch: ({ expected, received }) =>
    console.warn(`spec mismatch: ${received}`),
});

const e = await client.lookup("1500001");
await client.refresh();  // drop L1/L2 and re-fetch meta

Persistent cache (L2)

Pass any object that satisfies the PersistentCache interface as the cache option and data survives across process restarts. No default implementation ships with the SDK — pick one that suits your runtime.

interface PersistentCache {
  get(key: string): Promise<Uint8Array | null>;
  set(key: string, value: Uint8Array, ttl?: number): Promise<void>;
  delete(key: string): Promise<void>;
  clear(): Promise<void>;
}

IndexedDB in browsers, files on Node, KV / Cache API on Cloudflare, Redis — anything goes.

// Example: a simple Node.js file cache
import { readFile, writeFile, unlink, rm } from "node:fs/promises";
import { createHash } from "node:crypto";
import { JpzipClient, PersistentCache } from "@jpzip/jpzip";

const dir = "./.jpzip-cache";
const path = (k: string) =>
  `${dir}/${createHash("sha1").update(k).digest("hex")}.bin`;

const cache: PersistentCache = {
  async get(k) { try { return await readFile(path(k)); } catch { return null; } },
  async set(k, v) { await writeFile(path(k), v); },
  async delete(k) { await unlink(path(k)).catch(() => {}); },
  async clear() { await rm(dir, { recursive: true, force: true }); },
};

const client = new JpzipClient({ cache });
await client.preload({ scope: "all" });
// → entire dataset is L2-persisted; next start works offline

Edge / Serverless notes

  • Cloudflare Workers / Vercel Edge can’t use file-based L2. Wrap caches.default or Workers KV in a PersistentCache instead.
  • On short-lived runtimes the L1 won’t reliably survive between requests, so call preload once at init time.
  • getMeta() is fetched once and cached inside the SDK — don’t call it per request.

The shared surface (lookup / lookupGroup / lookupAll / preload / getMeta / refresh / isValidZipcode), the L1 LRU, the optional L2 cache, 5xx exponential-backoff retries and version-change auto-invalidation are implemented identically across all SDKs.

Cache strategy

The SDK distinguishes three cache layers. Knowing the defaults makes it easy to reason about when to call preload / refresh.

LayerPurposeTypical sizeDefault
L1 in-memory LRUSuppress duplicate fetches within one process~1 MBAlways on (internal)
L2 persistent cachepreload / fast bootUp to ~10 MBOff (user-enabled)
L3 HTTP cacheBrowser / OS / fetch layerEnvironment-dependentFollows Cache-Control

Monthly data updates are detected from the version field in /meta.json; the SDK automatically invalidates L1 and L2. To re-fetch explicitly, call refresh().

MCP server

A separate stdio server, @jpzip/mcp-server-jpzip, lets Claude and any other Model Context Protocol client look up Japanese postal codes. It is backed by the same jpzip.nadai.dev static data, so the results match the SDKs exactly. The process is stateless and keeps cached data only in memory — no on-disk cache.

Install

Claude Code:

claude mcp add jpzip -- npx -y @jpzip/mcp-server-jpzip

Claude Desktop, or any client where you edit mcp.json directly:

{
  "mcpServers": {
    "jpzip": {
      "command": "npx",
      "args": ["-y", "@jpzip/mcp-server-jpzip"]
    }
  }
}

Tools

ToolArgumentsPurpose
lookup_zipcode zipcode: string (7 digits, hyphens allowed) Zipcode → address (kanji / kana / romaji + JIS / MIC codes)
search_by_address query: string, limit?: int (1–200, default 20) Free-text address → candidate zipcodes (matches across kanji / kana / romaji, whitespace ignored)
list_cities_in_prefecture prefecture: string Prefecture → list of municipalities with their MIC city codes
get_metadata Data version, entry count, build timestamp

Runtime model

  • lookup_zipcode only fetches the matching 3-digit prefix file (~10 KB) and stores it in the SDK’s L1 LRU.
  • search_by_address / list_cities_in_prefecture fetch the full dataset (~25 MB) on first call and keep it in memory; subsequent calls within the same MCP process are instant.
  • No on-disk cache. When Claude restarts, the in-memory data is dropped and re-fetched on demand.
  • jpzip data does not include stations, train lines, or per-business zipcodes (zipcode ⇄ address only). The address search is single-language substring only — e.g. a mixed query like Yokohama Honcho that skips the ward name will not match.

Source: github.com/jpzip/mcp / npm: @jpzip/mcp-server-jpzip.

Protocol spec

Reference for raw HTTP usage or for writing your own SDK. The authoritative spec lives at github.com/jpzip/spec.

Endpoints

PathContentsApprox. size
/meta.jsonVersion & stats~10 KB
/g/{1}.jsonAll entries for a 1-digit prefix3–4 MB
/p/{3}.jsonAll entries for a 3-digit prefix~10 KB

There is intentionally no /all.json — at over 25 MiB it would exceed Cloudflare Pages limits. When you need the whole dataset, fetch /g/0.json through /g/9.json in parallel and merge (the SDK’s lookupAll / preload do this for you).

curl https://jpzip.nadai.dev/p/231.json \
  | jq '."2310017"'

Schema

/g/* and /p/* are flat dictionaries with the same shape. Keys are 7-digit zipcodes and values are ZipcodeEntry.

{
  "prefecture":      "神奈川県",
  "prefecture_kana": "カナガワケン",
  "prefecture_roma": "Kanagawa",
  "prefecture_code": "14",           // JIS X 0401 (2 digits)
  "city":            "横浜市中区",
  "city_kana":       "ヨコハマシナカク",
  "city_roma":       "Yokohama Shi Naka Ku",
  "city_code":       "14104",         // MIC municipality code (5 digits)
  "towns": [
    { "town": "本町", "kana": "ホンチョウ", "roma": "Honcho" }
  ]
}

When one zipcode covers several town areas (e.g. Kyoto’s street-name addresses), the towns array has multiple entries. Notes like “if not listed below” are normalised into towns[].note.

HTTP spec

  • Method: GET only (no query parameters)
  • Content-Type: application/json; charset=utf-8
  • Encoding: gzip / brotli (Cloudflare, automatic)
  • Cache-Control: public, max-age=86400 (24h)
  • CORS: Access-Control-Allow-Origin: *
  • 404: Prefix does not exist. SDKs treat this as “not found”.
  • 5xx / network: SDKs retry up to 3 times with exponential backoff.

Versioning

  • spec_version (e.g. 1.0): the protocol version, SemVer. Within v1.x no existing field is removed or type-changed.
  • version (e.g. 2026-05): the monthly data version. Unrelated to compatibility — it just tells you how fresh the data is.

License

  • Spec / SDK / ETL: MIT
  • Distributed data: effectively public domain (sourced from Japan Post)

Commercial use, redistribution and modification are all free. Attribution is not required (though appreciated).

Fixed spec, tiny SDKs

spec v1.0 is frozen. Because the protocol is small, each SDK is only a few dozen lines — with identical signatures and identical cache strategies across languages.