A free REST API for synthetic Canadian addresses and postal code validation. No key, no signup, and seeded requests that reproduce forever.
No API key, no signup, no rate limit beyond fair use. Every endpoint is a GET and returns JSON.
| Endpoint | What it does |
|---|---|
| GET /api/v1/addresses | Generate synthetic Canadian addresses. |
| GET /api/v1/postal-codes/validate | Validate a postal code and infer its region or regions. |
| GET /api/v1/provinces | The 13 provinces and territories with their postal district letters. |
| GET /api/v1/cities | The municipalities the generator can filter to. |
| Parameter | Meaning |
|---|---|
| count | How many records, 1 to 100. Defaults to 10. |
| province | Restrict 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. |
| city | Restrict to one municipality, by slug or name. See /api/v1/cities. |
| seed | Makes the response reproducible. Omit it and a seed is minted and returned in meta.seed. |
| company | true to include a company name. Defaults to false. |
| nonCivic | true 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. |
| country | Optional. Must be "CA" if supplied; this API serves Canada only. |
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"
// 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"]
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": "..."
}
}
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.
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.
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.
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.
| Limit | Value |
|---|---|
| Records per request | 100 |
| Requests per minute | 100 |
| Authentication | None. No API key is required. |
| CORS | Open to all origins for the generation and validation endpoints. |
| Caching | Seeded requests are cacheable for 24 hours. Unseeded requests are never cached, because they mint a new seed each time. |
| Current dataVersion | 2 |
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\"."
}
No. There is no key, no signup and no quota beyond a fair-use rate limit per IP address.
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.
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.
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.
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.
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.