Canada Address API

A free REST API for synthetic Canadian addresses and postal code validation. No key, no signup, and seeded requests that reproduce forever.

Endpoints

No API key, no signup, no rate limit beyond fair use. Every endpoint is a GET and returns JSON.

EndpointWhat it does
GET /api/v1/addressesGenerate synthetic Canadian addresses.
GET /api/v1/postal-codes/validateValidate a postal code and infer its region or regions.
GET /api/v1/provincesThe 13 provinces and territories with their postal district letters.
GET /api/v1/citiesThe municipalities the generator can filter to.

Parameters for /api/v1/addresses

ParameterMeaning
countHow many records, 1 to 100. Defaults to 10.
provinceRestrict to one province or territory. Accepts a code or a full name, case-insensitively: ON, QC, BC, AB, MB, SK, NS, NB, NL, PE, YT, NT, NU.
cityRestrict to one municipality, by slug or name. See /api/v1/cities.
seedMakes the response reproducible. Omit it and a seed is minted and returned in meta.seed.
companytrue to include a company name. Defaults to false.
nonCivictrue to include rural routes, PO boxes and general delivery. Defaults to false, so the default response is all-civic.
locale"en" or "fr". Defaults to the convention of the city.
countryOptional. Must be "CA" if supplied; this API serves Canada only.

Try it

Copy any of these. They work as written.

# ten addresses anywhere in Canada
curl -s "/api/v1/addresses?count=10"

# five in Ontario, reproducible forever
curl -s "/api/v1/addresses?province=ON&count=5&seed=12345"

# full province names work too, in either language
curl -s "/api/v1/addresses?province=british%20columbia&count=3"

# include rural routes, PO boxes and general delivery
curl -s "/api/v1/addresses?province=NU&count=10&nonCivic=true"

# validate a postal code
curl -s "/api/v1/postal-codes/validate?code=K1A%200B1"

# the X ambiguity: this returns BOTH NT and NU
curl -s "/api/v1/postal-codes/validate?code=X0A%200H0"

In your own code

// JavaScript
const res = await fetch("/api/v1/addresses?province=QC&count=5&seed=42");
const { data, meta } = await res.json();
console.log(meta.seed, meta.dataVersion);

# Python
import requests
r = requests.get("/api/v1/addresses", params={"province": "AB", "count": 5, "seed": 42})
records = r.json()["data"]

Response shape

Records are in data. Everything about the request and the generator is in meta, including the seed that was used and the data version that produced it.

{
  "data": [
    {
      "name": "Priya Sandhu",
      "addressType": "civic",
      "company": null,
      "addressLine1": "100-1234 Yonge St W",
      "addressLine2": null,
      "locality": "Toronto",
      "province": "ON",
      "provinceName": "Ontario",
      "provinceType": "province",
      "postalCode": "M5V 3L9",
      "country": "Canada",
      "countryCode": "CA",
      "formatted": "...",
      "formattedCanadaPost": "..."
    }
  ],
  "meta": {
    "synthetic": true,
    "intendedUse": "development-and-testing",
    "count": 1,
    "seed": 12345,
    "seedMinted": false,
    "dataVersion": 1,
    "generatedAt": "...",
    "notice": "Synthetic data for software testing. Not real people. Not verified deliverable addresses.",
    "forwardSortationAreas": "..."
  }
}

Seeds, and what reproducibility is scoped to

The same seed and the same options return byte-identical records, permanently — within a data version. Every response carries dataVersion, currently 2.

Across data versions, records may differ. A seed selects a path through the generator’s data; when that data changes, the path lands somewhere else. Adding a city, correcting a postal district or extending the street vocabulary all change what an existing seed produces.

So if a committed fixture stops matching, compare the dataVersion in your stored response with the one in the new response. A change there is the explanation. If dataVersion is unchanged and the records differ, that is a bug and worth reporting.

Requests without a seed are never cacheable and mint a fresh seed each time. The seed used is always returned in meta.seed, so an unseeded exploratory call can be pinned afterwards.

Unknown parameters are rejected, not ignored

Send a parameter this API does not recognise and you get HTTP 400 naming it, suggesting the parameter you probably meant, and listing the ones the endpoint accepts.

This is deliberate, and it is the opposite of what most APIs do. The value of this one is reproducible fixtures. A request for ?provice=ON — one transposed letter — would otherwise return 200 with addresses from every province, and you would commit that as an Ontario fixture with nothing in the response to suggest anything went wrong. An error at the moment you make the typo costs a minute; a silently wrong fixture costs however long it takes to notice.

It is also consistent with how an unknown VALUE has always behaved here: ?province=Wessex is a 400 listing the valid codes, never a quiet fallback to all of Canada. Parameter names now work the same way.

Tracking parameters are the exception and are ignored rather than rejected: utm_source and the rest of the utm_ family, gclid, fbclid, ref, msclkid, mc_cid and mc_eid. Analytics and ad platforms append these to links outside your control, so a URL pasted from a chat message should not fail on a parameter you never typed. Nothing reads them and they cannot change a response.

Real forward sortation areas, generated local delivery units

Forward sortation areas are real: the first three characters of every generated postal code come from Statistics Canada Table 98-10-0019-01 (2021 Census). The local delivery unit — the last three characters — is generated, so the full code is well-formed and inside a real FSA but is not a verified deliverable address.

The split is deliberate. A forward sortation area is a public geographic unit — Statistics Canada publishes population and dwelling counts for all 1,646 of them. A full postal code identifies a delivery point, so generating the last three characters is what keeps every code this API returns from pointing at somewhere real.

The source is Statistics Canada Table 98-10-0019-01, "Population and dwelling counts: Canada and forward sortation areas", 2021 Census, released 2023-03-29, used under the Statistics Canada Open Licence. That table includes information copied with permission from Canada Post Corporation.

One consequence worth knowing: the source is a population and dwelling table, so an FSA that no household reported as a home address is not in it. That excludes PO-box-only and commercial districts, and it means absence from this data is evidence about the table rather than about Canada Post. Every FSA this API returns is real; the set it draws from is a subset of all the FSAs Canada Post assigns.

The one region assignment the table could not make is the X district, which Northwest Territories and Nunavut share and which the table has no province column to separate. That split — X0A, X0B and X0C to Nunavut; X0E, X0G and X1A to Northwest Territories — comes from a separately sourced mapping, not from Statistics Canada. See DATA_SOURCES.md.

A second: the second character of an FSA is 0 for rural and 1-9 for urban, and the generator prefers one matching the delivery form. It is a preference, not a guarantee, because it can only pick from FSAs that exist. Nunavut is the case that proves the point — its only forward sortation areas are X0A, X0B and X0C, all rural-marked, so a civic street address in Iqaluit necessarily carries one. The marker describes the sortation area, never the delivery point of an address inside it.

Limits

If you need more than 100 records, page by seed rather than by offset. A seeded request reproduces the same records permanently, so paging is stable in a way an offset into a random stream is not.

LimitValue
Records per request100
Requests per minute100
AuthenticationNone. No API key is required.
CORSOpen to all origins for the generation and validation endpoints.
CachingSeeded requests are cacheable for 24 hours. Unseeded requests are never cached, because they mint a new seed each time.
Current dataVersion2

Errors

Every failure is a 400 with a message naming the parameter and what would have been accepted. An invalid province lists all thirteen valid codes so you can fix it in one go.

{
  "status": "error",
  "message": "Unknown province or territory \"XX\". Valid codes are: ON, QC, BC, AB, MB, SK, NS, NB, NL, PE, YT, NT, NU. Full names are also accepted, for example \"Ontario\"."
}

Questions

Do I need an API key?

No. There is no key, no signup and no quota beyond a fair-use rate limit per IP address.

Will the same seed always return the same records?

Within a data version, yes, permanently. Across data versions it may not — every response carries a dataVersion so you can tell which one produced your fixture. That field is the reason the API shipped with it from its first release rather than adding it later.

Can I use this in production?

The data is synthetic and intended for development, QA, testing, demos and staging. Nothing stops you calling the API from a production system, but the records do not represent real people and the addresses are not verified deliverable addresses.

Why is count capped?

Because that is where server cost lives. The in-browser generator has a much higher cap, since it costs nothing to run. For bulk work, generate in the browser or page by seed through the API.

Are the postal codes real?

Since dataVersion 2 the forward sortation area — the first three characters — is real, drawn from Statistics Canada Table 98-10-0019-01 (2021 Census). The local delivery unit is generated, which is what keeps a code from pointing at a real delivery point. Every response states this as meta.forwardSortationAreas.

Related

Everything this site generates is synthetic, produced for development, QA, testing, demos and staging. The records do not represent real people, and the addresses are not verified deliverable addresses.