Address verification
One lookup reads a postal address against its own country’s format, splits it into the parts that country writes, names the delivery point behind it — a street, a post-office box, a private mailbox at a mail-receiving agency, general delivery, a rural route, military mail, a parcel locker, a business-volume arrangement — checks the postcode against the national directory, follows the building into the country’s own address register, and counts the companies registered there. Every answer comes from open data the app carries or syncs, so it takes milliseconds and the address never leaves the server.
The product is about places, not people. Nothing in the response says who lives or works at an address, and no field ever will.
$ curl https://spaw.co/api/v1/address \ -H "Authorization: Bearer sk_live_…" \ -H "Content-Type: application/json" \ -d '{"address": "71-75 Shelton Street, Covent Garden, London WC2H 9JQ", "country": "GB"}'
Sending an address
An address arrives either as one written line in address, the way it would be on an envelope, or as the separated fields address_line1, address_line2, organization, dependent_locality, locality, administrative_area, postal_code and po_box. One of the two is required, and so is country: an ISO 3166-1 alpha-2 code the format table covers, because without a format there is nothing to measure the address against. A country it does not know is refused with a validation error rather than answered.
Fields sent beside a written address win over what the parser read out of it — the caller knows more than the parser does. The optional deliverability flag asks the licensed partner whether post actually arrives; see Billing below.
Nothing else about the address is a validation failure. A missing town, a malformed postcode, a postcode that does not exist: those are answers, with a typed reason, and they are free.
A building named instead of numbered — Rose Cottage, Kemp House — goes in building when you send separated fields, and is separated by the parser when a written address puts it on the line above the street. The parser also sets aside a last line that only repeats the country and a c/o or attention line that names the addressee rather than the place, and folds a state written in full onto its code. Every such move is reported in changes, so a normalisation is never silent; the values are listed under What was confirmed and what was changed.
A floor, flat or suite written on a line of its own, or before or after the street on its line, is read as the unit, and every unit an address carries is kept, joined with , in the order written. A lone line with no number before the postcode is read by what the installed data says: a place the postcode directory lists for the code or around it makes it the town, and a street a register’s listing files there makes it the street, so with Open Names installed Strand, WC2R 0RL is a street address, and with the US directory Mountain View, CA 94043 answers missing_street; where the data cannot tell, the shape of the line decides. Neither reading is reported in changes.
The fields option
fields — a comma-separated list of the answer’s keys — keeps only the ones you name, in the answer’s own order, on the single lookup and the browser endpoint. It is applied after billing, so a narrowed answer costs what a full one does; meta is never narrowed, unknown names are ignored, and sources is dropped unless you name it. Batch, bulk and monitor runs always carry every field.
Fields and flags
Every response carries the same 61 fields in the same order, then sources. An invalid address answers the same shape with the fields nothing could be established for set to null. The signals are tri-state: false is a checked negative and null means the dataset behind it is not installed for that country, so nothing was evaluated. Nothing is guessed to fill a null.
| valid | The address stands as written for its own country: every part the format requires is there, the postcode matches the country’s pattern, and where a national directory is installed, that postcode exists. |
| reason | Why it does not, in one of nine values, listed below. Null when valid is true. Invalid answers are free. |
| country | The ISO 3166-1 alpha-2 code you sent, uppercased. Required on every request: without a country there is no format to measure the address against. |
| country_name / continent | The English name of the country and its continent code. |
| is_eu / is_eea / privacy_regime | EU and EEA membership, and the comprehensive privacy law that applies where the address is — gdpr, uk_gdpr, ccpa, law25, fadp and the rest. The same country facts the phone and IP lookups answer. |
| formatted | The address written the way that country’s postal operator writes it: its line order, its uppercasing, its position for the house number, with the parts that are missing left out rather than blanked. Null for an invalid address. |
| formatted_lines | The same rendering as formatted, one line per array entry, for a label printer or a form with one field per line. Null when formatted is. |
| formatted_local | The address in the country’s own script and line order where the format table carries one (Japan, China, Korea, Taiwan) and the input was written in that script. Null otherwise. See local-script rendering. |
| organization | The company line, as sent or as recognised in a written address: a line above a numbered street that carries no number of its own is read as the organisation, and changes says so. |
| building | A building named instead of numbered — Rose Cottage, The Old Rectory, Kemp House — as sent in the building field, or separated from the street line of a written address when the line above the street carries a building word and no number. |
| street / house_number | The street name and its number, split apart. The number goes on the side of the name that country writes it — leading in the United States and the United Kingdom, trailing in Germany and most of continental Europe. |
| unit | The flat, suite, floor or mailbox line. A floor written on a line of its own, before or after the street on its line, or between the house number and the street, is read as the unit rather than the street, and a private-mailbox number written on the street line is lifted out into this field. Several units written on one address are kept, joined with ", " in the order written: 5th Floor, Suite 500. |
| po_box | The box line in the operator’s own words (PO Box 1234, Postfach 10 01 01, BP 12). A box written where the street goes moves here and the street is cleared, because the box replaces it; a box on a line of its own beside a street keeps both. See PO box. |
| dependent_locality | The district or dependent locality, where that country writes one — Covent Garden under London, a barangay in the Philippines. |
| locality | The town, city or post town. |
| administrative_area | The state, province, region or county, folded to its code where that country writes one, so "California" and "CA" answer the same. |
| postal_code | The postcode as sent, or as read out of a written address. See postcode. |
| postal_code_formatted | The postcode written the way its operator prints it — the British space, the Dutch space, the Polish hyphen, the ZIP+4 hyphen — from the value you sent. Null when the postcode is missing or of the wrong shape. |
| postal_code_valid | Whether it matches that country’s postcode pattern. Null in the countries that use no postcodes at all. |
| postal_code_type | What the country calls it: zip, eircode, pin or postal_code. Null where there are none. |
| postcode_status | What the national postcode directory says about it. A terminated postcode is a real address whose postcode has been retired, not a typo, which is why it is kept and scored rather than refused. Null whenever the directory has no status to give: when no directory is installed for the country; when the code is not listed, which a complete directory also answers with the reason postcode_not_found and a partial one never treats as a denial; and for a code outside the installed directory’s scope — in Great Britain a Northern Irish BT postcode, a BFPO number, a BF1 or a BX code — which the directory is not asked about and never calls missing. A ZIP+4 is looked up by its ZIP, the first five digits, while postal_code_formatted keeps all nine. See postcode directory. |
| locality_matches_postcode | Whether the town on the address is one of the places the directory publishes for that postcode, compared on folded names and under the other names GeoNames lists for the same place (Munich for München). Null when either side is unknown or the directory lists no place for the code. A false is only scored when the town is listed nowhere in the postcode’s wider area either, and only by a directory that lists every place a code is written with; confirmation.locality says which case you have. See postcode area. |
| administrative_area_matches_postcode | Whether the state, province or region on the address is the one the directory publishes for that postcode, compared as a code or as a name. Null when either side is unknown, and when the value written is a place inside the published region that the directory cannot place the postcode in or out of — an Irish county, where an Eircode routing key is filed only under its province, or a county that a key crossing into another province reaches. A ZIP written with the wrong state is a common data-entry error, and this is the field that catches it. |
| address_type | What kind of delivery point the address names: street, po_box, private_mailbox, general_delivery, rural_route, military, parcel_locker, bulk_mail or unknown. unknown means no box naming is carried for that country, which is not the same answer as street. See delivery point. |
| address_type_source | What settled the type. address_rules when the operator’s own naming did. |
| is_po_box | A shortcut for address_type: po_box. Null in a country whose box naming is not carried, because there "not a box" was never established. |
| is_private_mailbox | True when a PMB number follows a real street address — one of the two spellings the postal service allows for a box at a mail-receiving agency — or when the licensed check reported the delivery point as an agency. The other spelling, a bare # number, is read as a unit, because flats and suites are written the same way. See private mailbox. |
| exists | True only when the building itself was found in the country’s official address register. False only when a register whose coverage is complete does not carry it. Null in every other case: no register for the country, a box with no street behind it, a register whose coverage is partial, or a register of streets alone, which can never answer about a building. See address register. |
| match_level | How far the address could be followed into that register: to the building, into a published house-number range on the street, to the street, to the postcode, or not at all. Null when no register was consulted. See address range and street register. |
| register | Which register answered, by dataset name, so the answer can be checked against the file it came from. |
| register_coverage | How complete that register’s own publisher says it is, except where Spaw marks a register partial because its copy cannot vouch for an address it lacks; the address data sources page says which and why. Always read exists against this field. See address data sources. |
| premise_key | A sixteen-character hex digest of the building — the country, the postcode or town, the folded street and the house number — and the key your outcome reports and your suppression list are filed under. Two spellings of one address share it, so it is what to join and deduplicate on. It is a key of a place, never of a person. Null when the address names no building. See premise key. |
| building_use | What the register that found the building says it is used for — residential, commercial, mixed, industrial, institutional, agricultural or other — where that register publishes a use. Null where it does not, and on anything short of a building match. Never inferred. See building use. |
| is_multi_unit | Whether the register lists several flats or suites at the building. True from two or more unit rows; false only when a register that declares its units lists none there; null otherwise — including the US National Address Database, whose contributors list units unevenly, so its silence means nothing. |
| unit_check | What the unit you wrote says against the units the register lists at the building: ok, missing (several units listed, none written) or unexpected (a flat or suite written at a single residence). Null when the register did not find the building, does not say how many units it has, or lists one unit at a non-residential building, where a suite is ordinary. A private-mailbox number never counts as a unit. See unit check. |
| street_suggestion | The street the register most likely meant, in its own spelling, when the typed street is not in the postcode but one close to it is — Banhofstrasse for Bahnhofstrasse. Null when the street was found, when nothing is close, or where no street listing is installed. See street suggestion. |
| latitude / longitude | Where the building stands, when the register that found it publishes a position — about a metre of precision — and otherwise the centroid the postcode directory publishes for the postcode. Never derived from the street or the house number. |
| location_source | What the coordinates describe, so nothing about their precision has to be guessed: the building, or the postcode’s centroid. Null when there are none. |
| companies_registered | How many companies still on an open company register name this address. Null where no register covers the country. |
| is_mail_drop | Whether mail is received at this address on other people’s behalf. True from any of the three sources below; false when an operator’s branch list or a company register covers the country and none of the three matched; null when neither covers it. See mail drop. |
| mail_drop_provider | The operator that publishes this address as one of its branches, or the registered-agent or formation service the address belongs to. Null when the flag came from the company count instead. |
| mail_drop_source | Which of the three answered: the operator’s own published branch list, more companies registered at the address than the published threshold, or an address a registered-agent or formation service publishes, or that a public listing or filing naming the service carries — a place where a company being registered is ordinary and a person claiming to live is not. See registered agent address. |
| deliverability_checked | Whether the licensed deliverability check ran. False whenever the request did not ask for it with deliverability: true, and until a partner is configured. |
| is_deliverable / is_cmra / is_vacant / is_residential | What that check said: whether post is actually delivered there, and the postal service’s own agency, vacant and residential flags for the delivery point. Null until it runs. |
| confirmation | What stood behind each part — postal_code, street, house_number, locality, administrative_area — named by the dataset that confirmed it: postcode_directory, postcode_area, register, range, company_register, mail_drop_list or pattern; unconfirmed when a dataset that could have confirmed the part did not, and null when nothing could check it. Null as a whole for an invalid address. See address confirmation. |
| changes | What the parser and the classifier moved or set aside on the way from your input to these parts, so a normalisation is never silent: country_line_removed, care_of_removed, building_separated, organization_separated, administrative_area_folded, box_line_moved, private_mailbox_moved_to_unit. Empty when nothing was changed. |
| looks_placeholder | Whether the street or the town is what somebody types to get past a form rather than a place: a test word, a keyboard run, a refusal, a name with no vowel. Whole names only — a real street that merely contains one of the words is never flagged. Null when there is no street and no town to judge. See placeholder address. |
| seen_by_you | How many times your own account looked this address up inside the window before this call, and when the first of those was — kept under the premise digest, never the address, and never anybody else’s lookups. Null on the guest demo and for test addresses. See address velocity. |
| risk_score / risk_level | The sum of the weights below, capped at 100; high from 60, medium from 30. Null for an invalid address. See risk score. |
| risk_signals | Each contributing signal with the weight applied and the dataset behind it; the weights sum to risk_score. |
| ok_to_ship | Your account’s shipping policy applied to this answer: false when a blocked signal fired, the score is above your ceiling, the register did not confirm a building you require confirmed, or the address cannot stand as written. See shipping policy. |
| blocked_by | What decided a false ok_to_ship: invalid, the first blocked signal in the weights’ order, risk_score, or unconfirmed — and suppressed on a batch, bulk or monitored answer served from your suppression list. Null when shipping is fine. |
| sources | The datasets consulted, each with the version installed when the answer was given: the format table and the box rules always, then the register, the postcode directory, the company register, the mail-drop list and the deliverability partner, each only when it answered. |
Address types
address_type takes nine values, read from the words the postal operator uses itself: street, po_box, private_mailbox, general_delivery, rural_route, military, parcel_locker, bulk_mail and unknown. Detection is deliberately conservative: some spellings only count at the start of a line and with a number after them, because a Swedish "Box", a Finnish "PL" and a Spanish "Apartado" are ordinary words elsewhere, and words that look like a box but name a flat — the Belgian "bus" — never classify anything.
A box written where the street goes replaces it: the line clears street and house_number and lands in po_box under the operator’s name (Canada’s GD reads GENERAL DELIVERY, and P O Box reads PO BOX); what follows the box on its line stays with it, and Canada Post’s station too (GD STN A reads GENERAL DELIVERY STN A). A numbered box written after the street on the same line is read the same way, in whatever spelling the operator uses: 123 Main St P.O. Box 5 answers po_box PO BOX 5 and no street. A box on a line of its own beside a street keeps both, and formatted writes the box under the street, nearest the town. A CEDEX after the town is business-mail routing, not a box: address_type reads bulk_mail and po_box stays null. A private mailbox is the exception: it stands at a real street address, so the street stays and the mailbox number moves into unit.
Each kind that is not a street has a glossary entry with the words that name it and what it scores: PO box, private mailbox, general delivery, rural route, military mail, parcel locker and CEDEX and the other business-volume arrangements.
Registers, coverage, and what exists means
Thirteen countries have an official register of buildings behind exists — the United States, France, Switzerland, Austria, Czechia, Norway, Belgium, Luxembourg, Australia, Latvia, Denmark, the Netherlands and Brazil, with New Zealand on servers that hold a LINZ key, and three German Länder — Berlin, North Rhine-Westphalia and Rhineland-Palatinate — carry a partial register each; Great Britain has a register of streets, and each publisher says how complete its own file is, except where Spaw marks a register partial because its copy cannot vouch for an address it lacks: the address data sources page says which and why. exists is true only on a premise match — the building itself. It is false only when the register that answered says register_coverage: "complete" and does not carry the address. Everywhere else it is null: no register for the country, a box with no street behind it, or a register whose coverage is partial.
A register is matched through the same street folding the parser applies to the input — suffixes, directionals and, since 2026-09-17, Portuguese street types and titles — so a register compiled before a change to that table has to be recompiled, or its round trip drops; the weekly coverage report shows it.
That distinction matters, so it is also built into the score: the not_in_register signal fires only when exists is false, so a partial register never contributes a point. The United States National Address Database and the French Base Adresse Nationale are partial — states and communes contribute voluntarily and coverage is uneven — and so is Ordnance Survey’s Open Names for Great Britain, which lists streets and never buildings. Denmark’s register is partial too: its answers come from a copy taken on 26 September 2026, because the interface it was read through closed on 1 October 2026, and it stays partial until the register is read from its new distribution. Absence from a partial register is not evidence that an address does not exist, and it is never treated as such. Switzerland, Austria, Czechia, Norway, Belgium, Luxembourg, Australia, Latvia and the Netherlands have registers whose coverage is complete, and there absence is an answer. The registers section says what each one carries beyond existence.
Reasons
An address that cannot stand as written always says why, in one of nine values, in the order a person would fix them: what is missing first, then what is wrong, then what does not exist. Each has its own reference page. Every one of them is free, and the parts that were recognised are still reported.
| empty_address | Nothing in the input was recognisable as an address part. |
| missing_country | No country was given, so there is no format to measure the address against. |
| unknown_country | The country code is not one the format table covers. |
| missing_street | The country’s format requires a street line and none was given — and no box, route or general-delivery line stands in for it. |
| missing_locality | The format requires a town or city, and the address names neither a town nor a postcode that would identify one. A street and a postcode stand on their own. |
| missing_administrative_area | The format requires a state or province, and the country has no postcodes to carry the geography instead. |
| missing_postal_code | The format requires a postcode and none was given. |
| invalid_postal_code | The postcode does not match the country’s pattern — the wrong length, the wrong shape, the wrong alphabet. |
| postcode_not_found | The postcode has the right shape but the national directory does not list it. Only a country whose whole directory is installed can say this. |
A missing state or province only breaks an address in a country with no postcodes, where it is the only geographic anchor left. Everywhere else the postcode already carries the geography and post arrives without the province written out, so it is not demanded.
Risk score
The score is arithmetic over the flags in the same response, capped at 100; high from 60, medium from 30. Recompute it, or ignore it and branch on the flags. It measures how unlikely an address is to be somebody’s own front door, not fraud, and an invalid address has no score at all.
| undeliverable | The deliverability partner says post is not delivered there | +80 |
| private_mailbox | A private mailbox at a mail-receiving agency | +60 |
| placeholder | A street or town typed to get past the form: a test word, a keyboard run, a refusal | +60 |
| mail_drop | A mail drop: an operator’s published branch, or more companies registered than the threshold | +50 |
| reported_returned | Your own most recent outcome report for the address was returned, moved or refused | +50 |
| vacant | The deliverability partner records the delivery point as vacant | +40 |
| general_delivery | General delivery, held at the counter for collection | +40 |
| po_box | A post-office box, where a box is not the ordinary way to receive mail | +30 |
| unit_unexpected | A flat or suite written at a building the register lists as a single residence | +30 |
| terminated_postcode | A postcode the national directory has terminated | +30 |
| locality_mismatch | The town does not match the one the directory publishes for the postcode, nor any place it lists in the postcode’s area | +30 |
| region_mismatch | The state, province or region does not match the one the directory publishes for the postcode | +30 |
| not_in_register | Not in a register whose coverage is complete, so exists is false | +20 |
| unit_missing | No flat or suite written at a building the register lists several units at | +20 |
| street_not_found | The street is not in a register of streets whose coverage is complete | +20 |
| number_out_of_range | The street is known, but the house number falls outside every range published for it | +20 |
| bulk_mail | A business-volume arrangement such as a CEDEX or an Antwoordnummer | +20 |
Worked examples, as the API answers them:
| An ordinary street address with a live postcode | none | 0, low |
| A United States address carrying a PMB number at a mail-receiving agency | private_mailbox 60 | 60, high |
| A London address with thousands of companies registered at it | mail_drop 50 | 50, medium |
| A French CEDEX address in a register that lists it | bulk_mail 20 | 20, low |
| A post-office box in a country where a box is not the usual way to get post, written with a town the postcode directory disagrees with | po_box 30, locality_mismatch 30 | 60, high |
| A post-office box in the United Arab Emirates | none — a box is how post normally arrives there | 0, low |
| A postcode the directory does not list | postcode_not_found | null, invalid, free |
risk_signals spells the sum out so nobody re-implements it. For the London example above it reads:
"risk_signals": [ { "signal": "mail_drop", "weight": 50, "dataset": "uk-companies-house" } ], "risk_score": 50, "risk_level": "medium"
Two weights are deliberately conditional. A post-office box scores nothing in the countries where a box is the ordinary way to receive post — the United Arab Emirates, Oman, Qatar, Kuwait, Bahrain, Saudi Arabia, Jordan, Lebanon and Hong Kong — because there it says nothing about the recipient. And not_in_register needs a register whose coverage is complete, so absence from a partial one costs nothing.
Where each fact comes from
Every dataset behind an answer is public and licensed for commercial use, and every response names the ones it consulted with the version installed at the time, so an answer can be checked against the file that produced it. The licences that ask for attribution get it in the footer of every page.
| address-formats | What an address needs in 207 countries and territories: the line layout, the required fields, the postcode pattern, the names that country uses for its postcode, locality and administrative area, and which fields it uppercases. |
| address-rules | How 52 postal operators name a delivery point that is not a street, plus the words that look like a box but name a flat, which side of the street name the house number goes on, the state and province codes of the countries that write one, and the countries where a post-office box is the ordinary way to receive post. |
| us-national-address-database | exists and match_level in the United States, the building’s position and, where the contributing authority filled it in, its use. Coverage is partial: states and local authorities contribute voluntarily, so an address that is not in it may simply be in a part that was never shared, and units are listed too unevenly for is_multi_unit to be answered from it. |
| us-tiger-address-ranges | match_level range in the United States: whether the house number sits inside a range the Census Bureau publishes for the street in that ZIP, and number_out_of_range when the street is known and the number sits outside every range. Coverage is partial — not every road edge carries a range, and a street on a ZIP boundary can carry only its neighbour’s ZIP — so a street it does not know is never denied. |
| fr-base-adresse-nationale | exists, match_level and the building’s position in France and its overseas regions. Coverage is partial. |
| os-open-names | match_level street in Great Britain, keyed by postcode district, and the places every British postcode is written with. It lists streets and never buildings, so exists stays null; Open Names files a road under one district while a long road runs through several, so its coverage is declared partial and a street it does not carry is never denied. |
| ch-gebaeudeadressverzeichnis | exists and match_level in Switzerland, from the official directory of building addresses, with the building’s position and its category as a use. Coverage is complete. |
| at-adressregister | exists and match_level in Austria, from the address register of the Bundesamt für Eich- und Vermessungswesen, with the building’s position, its use and its dwellings, which is what answers is_multi_unit. Coverage is complete. |
| cz-ruian-adresni-mista | exists, match_level and the building’s position in Czechia, from the RÚIAN address points of the Czech Office for Surveying, Mapping and Cadastre. Coverage is complete. |
| no-matrikkelen-adresse | exists, match_level and the building’s position in Norway, from Matrikkelen, the national property register kept by Kartverket. Coverage is complete. |
| be-best-address-flanders / -wallonia / -brussels | exists, match_level, the building’s position and its box numbers in Belgium, from the three regional registers the federal BeST service republishes daily; every language spelling of a street is indexed. Coverage is complete. |
| lu-bd-adresses | exists, match_level and the building’s position in Luxembourg, from the cadastre’s national address file. Coverage is complete. |
| au-gnaf | exists, match_level, the building’s position and its flats in Australia, from the Geocoded National Address File. Coverage is complete. The licence forbids using the file to build a mailing list, and nothing here does. |
| lv-valsts-adresu-registrs | exists, match_level and the building’s position in Latvia, from the State Address Register. Coverage is complete. |
| dk-danmarks-adresser | exists, match_level, the building’s position and its floors and doors in Denmark, from a copy of Danmarks Adresseregister (DAR) taken on 26 September 2026. Coverage is partial: the interface the copy was read through closed on 1 October 2026, so an address created since is not in it, and until the register is read from its new distribution an address that is not found answers exists null, never not_in_register. |
| nz-linz-addresses | exists, match_level and the building’s position in New Zealand, from the LINZ street address dataset — on servers configured with a LINZ Data Service key. Coverage is complete. |
| ons-postcode-directory | postcode_status, the introduction and termination dates, the centroid and the local authority in Great Britain, and lets postcode_not_found be said there. Northern Ireland postcodes are excluded: reusing them needs a separate Land & Property Services licence. So the directory is not consulted for them: a BT postcode answers postcode_status null — not evaluated, never postcode_not_found — with no town or county comparison, no centroid and no signal from the directory. The same goes for a BFPO number and the BF1 and BX codes, which the release does not carry. |
| geonames-postal-codes | postcode_status, the places and the region behind locality_matches_postcode and administrative_area_matches_postcode, and the centroid, in about a hundred countries outside Great Britain; postcode_not_found where the copy is complete. Which countries are complete, partial or prefix-only is set out under Postcode directories above. |
| uk-companies-house | companies_registered in the United Kingdom, from the registered office of every company in the free company data product — a copy of the live register, which a dissolved company has already left. |
| fr-sirene-establishments | companies_registered in France and its overseas territories, from the address of every active establishment in INSEE’s Sirene base — which is where a French domiciliation address shows itself; an establishment whose owner has opposed publication has no address in the file and is not counted. |
| no-enhetsregisteret | companies_registered in Norway, from the business address of every entity in the Brønnøysund register: a deleted entity has left the file, while one in bankruptcy or winding up is still registered and still counted. |
| company-registers (US) | companies_registered in the United States, from the Colorado, New York, Oregon, Connecticut and Hawaii business registries — principal and registered-agent addresses of the entities each registry still lists as existing; a dissolved, withdrawn, revoked or merged entity is not counted. |
| mail-drop-locations | is_mail_drop and mail_drop_provider from the branch addresses mailbox and mail-forwarding operators publish themselves: The UPS Store, PostNet, PostScan Mail, Opus Virtual Offices and Davinci Virtual Offices in North America, Mail Boxes Etc in the United Kingdom and Ireland. |
| registered-agents | mail_drop_source registered_agent: the addresses of registered-agent and formation services in the United States and the United Kingdom, where a cluster of companies is the agent’s and not a mailbox shop’s. Each entry is an address the agent publishes as its own, or one that a public listing or filing naming the service carries, and the file records which. |
| nl-bag-adressen | exists and match_level in the Netherlands, from the Basisregistratie Adressen en Gebouwen (BAG) kept by Kadaster: every nummeraanduiding with its street and town, one monthly extract. Coverage is complete. |
| br-cnefe-2022 | exists, match_level and the building’s position in Brazil, from IBGE’s Cadastro Nacional de Endereços para Fins Estatísticos, the address list of the 2022 census, one file per state. Coverage is partial: a census list is a snapshot, not a live register, so absence never becomes not_in_register. |
| de-berlin-adressen | exists and match_level in Berlin, from the Geoportal Berlin address register kept by the Amt für Statistik Berlin-Brandenburg, with the postcode on every row. Coverage is partial (one Land), so absence never becomes not_in_register. |
| de-nrw-gebaeudereferenzen | exists and match_level in North Rhine-Westphalia, from the Gebäudereferenzen published by Geobasis NRW. The open product carries no postcode, so buildings are filed by municipality and matched through the town written. Coverage is partial. |
| de-rlp-hauskoordinaten | exists and match_level in Rhineland-Palatinate, from the Hauskoordinaten of the Landesamt für Vermessung und Geobasisinformation (LVermGeoRP), without postal enrichment, so buildings are filed by municipality. Coverage is partial. |
| register-derived postcodes (<register>-postcodes) | The postcodes a complete register of buildings lists, added to the country’s directory where the directory lacks them, with the register’s places and centroids beside the directory’s own. They augment and never replace: box-only codes a register of buildings never lists keep answering, which is why no country becomes complete by augmentation alone. |
| geonames-cities | The other names a town is written under — Munich for München, Moscow for Москва — behind locality_matches_postcode, and the other names of a one-city region written as the town or as the region, behind administrative_area_matches_postcode too: places of 15,000 people or more, compared inside one country, a town only with the postcode’s own places and only within 30 km of the place. A name the directory also lists as a town of its own more than 30 km from the place counts only as the place’s own name cut short (Biberach for Biberach an der Riß), so Perlin, a village near Schwerin that GeoNames lists among Berlin’s names, does not count for a Berlin code. The same file gives the time zone of the nearest place in IP answers. An answer that matched a town or a region through it names geonames-cities in sources, dated by the day the cities file was installed; one whose town and region matched as the directory writes them does not. |
| ie-property-price-register | Which counties each of the fourteen Eircode routing keys that reach a county of another province, or that GeoNames files under the wrong one (P14), covers, and that D11 and D15 reach into Meath, behind administrative_area_matches_postcode in Ireland: a county counts for a key once it holds at least ten of that key’s sales, among the 250,671 of the register’s residential sales that carry an Eircode. Only that derived table ships with the code, so sources does not name it; no sale, address or price is held. |
| census-zcta-places (inside geonames-postal-codes) | The places whose land a US ZIP Code Tabulation Area covers, from the Census Bureau’s relationship file, added to each ZIP’s places beside the default city. Not the postal service’s list of acceptable names, so a US mismatch stays unweighted. |
| address-placeholders | looks_placeholder: the words and shapes people type instead of an address — test words, refusals, keyboard runs — matched as whole names only. |
What the lookup deliberately does not claim: it does not know whether post is actually delivered to an address; it cannot list the buildings at a UK postcode, only the streets; a partial register cannot say an address does not exist; a postcode centroid is not a rooftop, and the answer says which of the two its coordinates are; a building’s use and its units are what a register declares and never an inference; and nothing anywhere says who lives or works there. Each of those is a null or a typed refusal, never a guess.
What was confirmed and what was changed
match_level says how far the register got, but not which parts of the address it stood behind, and a normalised address does not say what was normalised. Two fields answer that. confirmation names, for the postcode, the street, the house number, the town and the region, the dataset that confirmed it — or unconfirmed when a dataset that could have did not, or null when nothing could check it. It is derived from data the lookup already consulted and costs nothing extra.
| postal_code | The directory lists the code, or a register knows it, or only the country’s pattern was met. unconfirmed when a directory or register covers the country and neither knew the code. A code outside the directory’s scope — BT, BFPO, BF1 and BX in Great Britain — is never put to the directory, so it reads pattern, never postcode_directory or unconfirmed: Open Names covers Great Britain only and is not asked about BT, BF or BX codes either. |
| street | A register lists the street (or the building, or a number range on it), a company register or a mailbox operator’s list names the building, or nothing that could have did. |
| house_number | The building itself is in a register, the number sits inside a published range on the street, a company register or an operator’s list names it, or nothing did. |
| locality | The town is one the directory publishes for the postcode — as written, or under another name GeoNames lists for the same place — or one it lists elsewhere in the postcode’s area (a settlement written where the operator writes the district, or the other way round), or, where the postcode’s region is a single city, that region’s own name (Distrito Federal or DF for 06000, Tokyo for a code in one of its wards). The last two are reported, never scored. Otherwise none of these, and unconfirmed: any other region’s name, such as Jalisco for a Guadalajara code, is not its postcode’s area. |
| administrative_area | The region is the one the directory publishes for the postcode, under any of the names and codes listed for it below, or it is not. An Irish county inside the province an Eircode routing key is filed under answers null, because the directory can neither confirm nor refuse it, and so does every county a key that crosses into another province reaches. P14 is the exception: GeoNames files it under Leinster though none of its sales is there, so a Leinster county is refused for it. County Dublin for a Dublin district key other than D11 and D15 is confirmed. |
changes lists what the parser and the classifier moved or set aside on the way from your input to the parts, in the order it happened. An empty list means the address was read exactly as sent.
| country_line_removed | The last line only repeated the country — United Kingdom, USA, Deutschland, an alpha-3 code — and was set aside instead of being read as the town. |
| care_of_removed | A c/o, attn or FAO line named the addressee rather than the place. The product is about places, so it was dropped and never carried into the answer. |
| building_separated | A named building above the street — Rose Cottage, High Street — became building rather than being mistaken for the street. |
| organization_separated | A line without a number above a numbered street became organization, because the street below it already carried the number. |
| administrative_area_folded | A state or province written in full was folded onto the code its country writes: California to CA. |
| box_line_moved | A line that opens with a box, written where the street goes, moved to po_box under the operator’s name — P O Box 12 reads PO BOX 12 — and the street was cleared, because the box replaces it; the same goes for a numbered box written after the street on the same line (123 Main St P.O. Box 5). A box on its own line beside a street moves nothing. |
| private_mailbox_moved_to_unit | A PMB number on the street line moved into unit, leaving the real street it stands at. |
Postcode directories
A postcode directory answers postcode_status, the places and the region a postcode is written with, and a centroid. Two things decide how much a directory may claim. Its coverage: only a copy that reaches the operator’s published total may say a postcode does not exist, and everything else confirms what it holds and denies nothing. And its places: only a directory that lists every place a code is written with may score a town it does not list.
| Great Britain | Every postcode with its status, the dates it was introduced and terminated, its centroid and its local authority, so postcode_not_found can be said. The places a postcode is written with come from Ordnance Survey’s Open Names — the populated place, the borough, the county and the region — so a town is checked against all four. Northern Ireland is excluded: reusing BT postcodes needs a Land & Property Services licence, so a BT postcode is not looked up and answers postcode_status null rather than postcode_not_found. BFPO numbers and the BF1 and BX codes are not in the release either, and are answered the same way. |
| Complete elsewhere | In the United States, Germany, France, Spain, Italy, Austria, Switzerland, Belgium, Denmark, Norway, Finland, Slovakia, Japan, Mexico and India the copy reaches the operator’s published total, so a postcode it does not list answers postcode_not_found. A ZIP+4 is looked up by its first five digits, the ZIP, and postal_code_formatted keeps all nine; the registers, the number ranges, the company counts and the mail-drop and registered-agent lists match it on its ZIP too, whichever of the two spellings the address or the dataset carries. Every place listed under a code counts for the town check, and the region is compared by name or code. |
| Partial elsewhere | The postcode is confirmed when listed and never denied when it is not: an absent code answers postcode_status null, not postcode_not_found. Three directories hold only the leading part of a postcode and are matched on that part — Canada’s forward sortation area (M5V of M5V 3L9), the Netherlands’ four digits, Ireland’s routing key — and they are always partial. See forward sortation area. |
| Default place only | The US file carries only the postal service’s default city per ZIP, not every name it accepts, and a prefix directory names an area rather than its towns. A town such a directory does not list answers locality_matches_postcode false with confirmation.locality unconfirmed and no weight, because the directory cannot tell an acceptable alternate name from a wrong one. |
The town check reads the postcode’s own places first, then the places the directory lists anywhere in the postcode’s area — the letters of a British postcode, the first three digits of a ZIP, the first two characters elsewhere — because an envelope carries the settlement where the operator publishes the district, or the other way round. Where the postcode’s region is a single city — Mexico City, Delhi, Jakarta, Bangkok, Bogotá, Moscow, Saint Petersburg, Bucharest, Seoul, Korea’s six metropolitan cities and Sejong, and Tokyo for a code filed under one of its wards — the region’s own name counts as the postcode’s area too, so Distrito Federal for 06000 and Tokyo for 100-0005 read postcode_area, while the name of any other state — Jalisco for a Guadalajara code, or Tokyo for a Hachiōji code in the Tama cities — does not. A town written under another name of the same place counts as that place — Munich for München, Moscow for Москва — from the GeoNames list of places of 15,000 people or more, inside the same country only, and only against the postcode’s own places or the name of a one-city region, never against its neighbours’, and only where the postcode lies within 30 km of the point GeoNames gives the place. Bristol written for Briston’s NR24 2AD is therefore refused, and a name the directory also lists as a town of its own more than 30 km away counts only as the place’s own name cut short — Biberach for Biberach an der Riß, Yarmouth for Great Yarmouth — so Perlin, a village near Schwerin that GeoNames lists among Berlin’s names, is refused for a Berlin code. A Danish postal town counts without the letter or name of its district: København for København V, Aarhus for Aarhus C, Nykøbing for Nykøbing F. locality_mismatch fires only when the town is nowhere in that area and the directory lists every place; region_mismatch fires when the state, province or region is not the one published for the postcode, compared as a code or a name. A postcode GeoNames lists under two regions or more is published for each, so 3644 answers to Victoria as well as New South Wales. Where the directory files a region under a number, an older name or a wider area, the names people write for it count too: a Mexican state by its INEGI number, ISO code, abbreviation or name (CMX, CDMX, DF or Ciudad de México for Mexico City), an Irish province by its name, letter or Irish name, an Indian state by its ISO code, old or new, an Indonesian province by its statistics-office number, ISO code, Indonesian or English name or short form (Jawa Barat, West Java, Jabar), a Korean province or city by its romanised, English or Hangul name, Bogotá by its capital-district names, and a Spanish province that has two official names by either (Bizkaia or Vizcaya).
The directory files an Eircode routing key only under its province, so a county inside that province is neither confirmed nor refused — administrative_area_matches_postcode answers null — while a county in another province is refused. Fourteen keys cross a county line into another province, or are filed under the wrong one — Athlone’s N37 reaches Roscommon, Waterford’s X91 Kilkenny — and for those every county the key reaches, and that county’s province, answers null too; the province GeoNames files the key under answers null rather than true, and a county the key does not reach is still refused unless it lies in that province — and for P14, which GeoNames files under Leinster though none of its sales is there, even then. Which counties a key reaches is read from the Property Services Regulatory Authority’s Residential Property Price Register: a county counts once it holds at least ten of the key’s sales. A Dublin district key (D01–D24, D6W) also confirms County Dublin (Dublin, Co. Dublin, D) and its own district (Dublin 2 for D02), except D11 and D15, which reach into Meath (17 of D11’s 3,132 sales, 149 of D15’s 7,075) and confirm only their own district (Dublin 11, Dublin 15) and its province.
Registers: buildings, streets and number ranges
A national address register is compiled here as three indexes — buildings, streets, postcodes — plus what the register publishes about each building beyond its existence.
Where a building stands
Every register of buildings publishes a position, so a premise match answers latitude and longitude with location_source: register — the building, to about a metre, converted from each publisher’s own grid — and anything short of one falls back to the postcode centroid. Three registers publish what a building is used for, which is what building_use carries: the Swiss directory’s building category, the Austrian register’s use, and the US National Address Database where the contributing authority filled the field in. Five declare the flats or suites at each building — Austria’s dwelling counts, the Belgian box numbers, the Australian flat numbers, the Danish floors and doors — and there is_multi_unit is true from two or more and false from none. The US database lists units so unevenly that its silence means nothing, so it answers null there. None of this is inferred: a register that publishes no use answers null, never a guess from the street.
A register of streets: Great Britain
No open dataset lists Great Britain’s buildings, but Ordnance Survey’s Open Names lists its streets, keyed by postcode district. A British address whose street is there answers match_level: street with confirmation.street: register, and exists stays null, because a register of streets can never answer about a building. Open Names files a road under one district while a long road runs through several, so a street it does not carry answers match_level: none and nothing more: the register is declared partial, and street_not_found is reserved for a register of streets whose coverage is complete; no such register is installed today. The same file gives every British postcode its places, so locality_matches_postcode is answered in Great Britain against the populated place, the borough, the county and the region at once.
Number ranges: the United States
The National Address Database covers the states that share their data. For the rest, the Census Bureau’s TIGER/Line files publish, for every road edge, the range of house numbers along it and the ZIP it sits in. When no building register carries an address, a number inside a published range on its street answers match_level: range with confirmation.house_number: range, and a number outside every range published for a street the files know answers match_level: street and scores number_out_of_range. Ranges are potential, not actual — a range says the number could stand on that edge, not that a building does — so a range match never sets exists, and because not every edge carries a range and a street on a ZIP boundary can carry only its neighbour’s ZIP, the files are declared partial and a street they do not know is never denied.
Street suggestions and the streets endpoint
Every register with streets keeps a listing of them by postcode, in the register’s own spelling. Two things read it. When a typed street is not in the postcode but one close to it is — Banhofstrasse for Bahnhofstrasse, Limatquai for Limmatquai — a single lookup answers street_suggestion with the register’s spelling; a street that was found, or one nothing is close to, answers null.
And GET /api/v1/address/streets?country=CH&postal_code=8001 answers every street the register lists inside one postcode, sorted, for a street picker under a postcode field: one call returns the whole postcode, so a form filters locally as the visitor types rather than calling per keystroke. The answer is { country, postal_code, register, streets, count } with the register named as it is in a lookup’s sources. A ZIP+4 answers every street in its five-digit ZIP, and the answer names that ZIP, because no register files streets by the four-digit add-on. It costs one credit per answered postcode and is free for the next seven days for the same postcode, every spelling of one ZIP counting as that ZIP. It costs nothing at all when no register with a listing covers the country, when the postcode lists no street, or when the listing does not cover the postcode — all three answer 404 ADDRESS_STREETS_NOT_FOUND, with a message that says which. It carries no house numbers and nothing about who is there.
$ curl "https://spaw.co/api/v1/address/streets?country=CH&postal_code=8001" \ -H "Authorization: Bearer sk_live_…" { "success": true, "data": { "country": "CH", "postal_code": "8001", "register": "ch-gebaeudeadressverzeichnis", "streets": ["Bahnhofstrasse", "Limmatquai", "Paradeplatz", …], "count": 200 }, "meta": { "credits_used": 1, "cache_hit": false, … } }
The listing exists wherever a register with streets is installed: Switzerland, Austria, Czechia, Norway, Belgium, Luxembourg, Latvia, Australia and the Netherlands in full, Denmark as its register stood on 26 September 2026, the parts of the United States, France and Brazil their registers carry, Berlin, whose register is filed by postcode (the North Rhine-Westphalia and Rhineland-Palatinate registers are filed by town, so they list no street at a postcode), and Great Britain by postcode district from Open Names. Where several registers share a country region by region, as Belgium’s three do, the postcode is read from the register that lists streets there, and register names it. Open data only: the registers publish their streets and Spaw lists them back.
A miss reads “No street is listed at 9999 in CH.” only where the registers’ coverage is complete and none of them holds an address at the postcode. Every other miss says why instead of reading as a postcode with no street: a Northern Irish BT, forces BFPO or BF, or non-geographic BX code is outside Ordnance Survey’s Great Britain data; a German postcode outside Berlin, or a postcode in a Belgian region whose register is not installed, is in a part of the country no installed register covers; a British district Open Names files no street under is one the register cannot vouch for, since it files each road under one district; and in many Czech villages the register holds addresses but none on a named street, so there is no street to list. The streets endpoint answers these with 404 ADDRESS_STREETS_NOT_FOUND and the postcode list with 501 ADDRESS_LIST_UNAVAILABLE, both free and neither leaving a repeat marker.
Mail drops and registered agents
is_mail_drop is a fact about a place, and three sources say it. operator_list: a mailbox or mail-forwarding operator publishes the address as one of its branches — The UPS Store, PostNet, PostScan Mail, Opus Virtual Offices and Davinci Virtual Offices in North America, Mail Boxes Etc in the United Kingdom and Ireland — and mail_drop_provider names it. company_cluster: more companies are registered at the address than the published threshold, from the open company registers of the United Kingdom, France, Norway and five US states. Only companies each register still lists are counted. A business park named after its road — Ferry Road Office Park — is counted under the park and under the road inside the same full British postcode, so 7 Ferry Road and 7 Ferry Road Office Park read the same companies; a park that numbers its units can share a count with the same number on its road.
registered_agent is the third, and it exists to keep the second honest. A registered-agent or formation service publishes its own address so that companies can name it as their registered office: 251 Little Falls Drive in Wilmington, 71-75 Shelton Street in London. Tens of thousands of companies stand there, and every one of them is ordinary — a company registered at its agent is what the law expects. A person claiming to live there is not. So the cluster is named for what it is, mail_drop_provider carries the agent, and the mail_drop weight applies as it does to any address that receives post on other people’s behalf. It remains a statement about the place, never about anyone who uses it.
Test addresses
Five fixed test addresses on a street that does not exist, under a ZIP the postal service has never assigned, answer the same canonical result every time, cost no credits, leave no repeat marker, read no outcome report and touch no suppression list — the way payment providers publish test card numbers. They answer on every endpoint, including batch, bulk, monitors, the browser endpoint and the MCP tools, and the published sandbox key answers them without an account. Their sources name the test-postal-addresses dataset. Any spelling of the street and the ZIP is recognised, a ZIP+4 of 99999 included; the country must be US.
| 1 Spaw Test Street, Springfield, IL 99999 | valid · exists true from the test register, located, risk_score 0 |
| 2 Spaw Test Street, Springfield, IL 99999 | valid · is_mail_drop true (operator_list, Spaw Test Mailboxes), companies_registered 120, risk_score 50 (mail_drop) |
| 3 Spaw Test Street, Springfield, IL 99999 | valid · exists false, match_level street, risk_score 20 (not_in_register) |
| 4 Spaw Test Street, Springfield, IL 99999 | valid · postcode_status terminated, risk_score 30 (terminated_postcode) |
| 5 Spaw Test Street PMB 5, Springfield, IL 99999 | valid · address_type private_mailbox, unit PMB 5, risk_score 60 (private_mailbox) |
The shipping policy
Every answer ends with a decision, so a checkout branches on one boolean instead of re-implementing the weights table. ok_to_ship and blocked_by come from your account’s shipping policy, three rules with published defaults.
| block_signals | undeliverable, placeholder, vacant | Block whenever one of these signals fires, whatever the score. Any weight name may be listed. |
| max_risk_score | 60 | Block when risk_score is above this; null ignores the score. |
| require_exists | false | Block unless a register confirmed the building (exists true). Only useful where a register covers the country. |
The rules apply in a fixed order, and blocked_by names the one that decided: invalid for an address that cannot stand as written — blocked whatever the policy says — then the first blocked signal in the weights’ order, then risk_score, then unconfirmed; a batch, bulk or monitored run answers suppressed for an address on your suppression list. Null means shipping is fine. The policy is account configuration, not money: any member may change it. GET /api/v1/address/policy reads it, PUT changes any subset of the three keys (block signals come back in the weights’ order; a body naming none of them is a 422), DELETE restores the defaults, and the dashboard has a “Shipping policy” tab. All three are free and unlogged, and every lookup — single, batch, bulk rows, monitors, the browser endpoint, the test addresses — applies the stored policy at once. The score is not the decision: the policy, not the weights, answers “may I ship”.
Placeholders and units
looks_placeholder is true when the whole street name or the whole town is what somebody types to get past a form rather than a place: a test word (test, asdf, qwerty), a refusal (n/a, none, no address, same as above), a shape nobody would name a place with — one character repeated, digits alone, words with no vowel among them and one of four letters or more, a run along a keyboard row. The shapes are judged on the street without the street type and directional at its end, so initials such as MLK Blvd and MG Road and a numbered street such as 23. Straße pass. Whole names only: the list is compared with the street’s suffix folded (Fake Street and Fake St are one name), and a real street that merely contains one of the words — Test Valley Road, Sample Road — is never flagged. Null when there is no street and no town to judge. The placeholder weight is 60, its dataset is the bundled address-placeholders list, and it sits in the default block list.
Units
unit_check reads the unit written on the address against the units the register lists at the building, so it only answers on a premise match at a register that declares its units: missing (several units listed, none written; weight 20 as unit_missing), unexpected (a flat or suite written at a building the register lists as a single residence; weight 30 as unit_unexpected), or ok. It stays null when the register lists one unit at a building whose use is not residential or unknown — offices have suites — and a private-mailbox number never counts as a unit. Use and units are never inferred: a register that publishes no units answers null, not false.
Your own lookups
seen_by_you counts how many times your own account looked the same address up inside the last seven days before this call, and when the first of those was. It is kept under the premise digest — or a hash of the folded input when the address names no building — never the address, and it reads nobody else’s lookups: a postal address is where somebody lives, and a pooled record of who is checking it would be a database about residences. It answers null on the guest demo and for the test addresses, and it carries no weight.
Local rendering and the postcode as printed
formatted_lines is formatted as an array, one line per entry, for a label printer or a form with one field per line. postal_code_formatted is the postcode written the way its operator prints it — the British space (SW1A 2AA), the Dutch space (1012 NX), the Polish hyphen (00-950), the ZIP+4 hyphen — from the value sent, and null when the postcode is missing or of the wrong shape. formatted_local is the address in the country’s own script and line order — largest part first, no uppercase rules — for Japan, China, Taiwan, Hong Kong, Macau and Korea, and only when the parts were written in that script; Latin-script input answers null even for Japan, because transliterating would be inventing. A Japanese address written on one line does not yet split into parts, so the local rendering is most reliable from the separated fields.
Postcodes from registers
A complete register of buildings lists every postcode its buildings carry, and that list feeds the country’s postcode directory: codes the directory lacks are added under <register>-postcodes, and the register’s places and centroids join the directory’s own. They augment and never replace, because a register of buildings never lists the box-only and large-recipient codes a postal operator issues — Swiss and Austrian Postfach codes, Australian GPO boxes — and replacing the directory would have turned real box addresses into postcode_not_found. For the same reason no country becomes complete by augmentation alone; that takes the operator’s published total.
In the United States the ZIP directory also carries the places whose land each ZIP Code Tabulation Area covers, from the Census Bureau’s relationship file, so locality_matches_postcode reads true for a neighbouring place; that file is not the postal service’s list of acceptable names, so a US mismatch stays unweighted. For Great Britain, every postcode district has a page of its own under the address checker: the places Open Names lists, live and terminated postcode counts, the streets filed under the district and the datasets behind them.
Batch, browser and postcode lookups
POST /api/v1/address/batch looks up as many as 100 addresses in one call, in input order, under the single-lookup billing rules. Each item is a written address or an object with the same fields as a single lookup, and may carry its own country; a list-level country applies to the items that name none. If the balance runs out mid-list, the paid partial results come back with meta.stopped_reason: "insufficient_credits"; a key that reaches its daily credit cap stops the list the same way, with meta.stopped_reason: "key_spend_cap". Only a batch whose first lookup is refused answers the typed 402, and a call that starts with the cap already spent answers 429 KEY_SPEND_CAP_REACHED without running anything. A batch counts as one request against the rate limit.
$ curl https://spaw.co/api/v1/address/batch \ -H "Authorization: Bearer sk_live_…" \ -H "Content-Type: application/json" \ -d '{"addresses": ["10 Downing Street, London SW1A 2AA", {"address_line1": "Postfach 10 01 01", "postal_code": "50441", "locality": "Köln", "country": "DE"}], "country": "GB"}'
POST /api/v1/address/public is the browser endpoint: a domain-locked publishable key instead of a secret one, checked against the browser’s Origin header, billed to the key’s owner and answered without a meta block so visitors never see the balance. Give such a key a daily credit cap, because it sits in page source.
The form helper that checks email and phone fields checks address fields too. Mark the one input that holds the whole written address with data-spaw-address and give it a country: data-spaw-country="GB" on the input, or a country select on the same form named by data-spaw-country-field. An address means nothing measured against no country, so until one is there nothing is sent — and the helper watches that select, so an address typed before the country is picked, the ordinary checkout order, is checked as soon as a country is chosen. The hint shows the address as the country writes it, a note when the place is a box, a locker or a counter rather than a front door, and the reason in plain words when it cannot stand as written. It never blocks a submit, and every answer is dispatched as a spaw:result event whose detail carries type: "address" and the same fields the API gives a server, minus the billing block. To keep even the publishable key off the page, point data-address-endpoint on the script tag at a route you host: it receives { "address": "…", "country": "GB" } and forwards it to POST /api/v1/address.
<script src="https://spaw.co/spaw-form.js" data-key="pk_live_…"></script> <select name="country">…</select> <input name="address" data-spaw-address data-spaw-country-field="country">
GET /api/v1/address/postcode/{postcode}?country=GB lists what stands at a postcode. Wherever a national register with a street listing is installed it answers the streets that register lists inside the postcode, in its own spelling and with no house numbers, for one credit. For Great Britain that listing is Ordnance Survey’s Open Names, filed by postcode district, so the answer is the streets of the district rather than of the postcode, at most 100 per answer; in the United States a ZIP+4 lists the streets of its five-digit ZIP. The buildings are what no open dataset lists: every full UK address list derives from the Royal Mail Postcode Address File, so the delivery-point list waits for a licensed partner, and a country with neither source answers 501 ADDRESS_LIST_UNAVAILABLE and is not charged. So does a postcode the street listing cannot speak for — a BT, BFPO, BF or BX code, a German postcode outside Berlin, a British district Open Names files no street under, a Czech village whose houses carry no street name — with a message that says which, because an empty list would read as “no addresses here”; an empty list is answered, free, only where the registers’ coverage is complete and none of them holds an address at the postcode. The streets themselves are the streets endpoint.
Agents reach the same lookup through the MCP server as the verify-address tool, and you can run one by hand on the address verification page in the dashboard, which shows every field with "not evaluated" where a dataset is missing.
Where it runs: Google Sheets with =SPAW_ADDRESS, working today; Zapier, Make and n8n through their own HTTP steps today, with Spaw apps built but not yet listed; the Node.js, Python and PHP SDKs, built and awaiting publication; and a Shopify recipe that checks each order’s shipping address after checkout through Zapier or Make.
Bulk address jobs
For a customer list or a CRM export, POST /api/v1/address/bulk queues up to 100,000 addresses and answers 202 straight away with the job. Each item is a written address or an object of the separated fields, and may carry its own country; the list-level country applies to the rest, and a row left with no country is answered missing_country, free, rather than refusing the whole run. deliverability: true asks the licensed partner about every row, and webhook_url (https only) is where the finish is announced.
$ curl https://spaw.co/api/v1/address/bulk \ -H "Authorization: Bearer sk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: addresses-2026-09-05" \ -d '{"addresses": ["10 Downing Street, London SW1A 2AA", {"address_line1": "Postfach 10 01 01", "postal_code": "50441", "locality": "Köln", "country": "DE"}], "country": "GB", "webhook_url": "https://www.example.com/hooks/spaw"}'
The webhook_secret in that answer is returned once and never again. Keep it: it signs the completion webhook.
An Idempotency-Key header (1 to 128 printable ASCII characters) makes creation safe to retry. A retry carrying the same key and the same request answers 200 with the same body — the same job, the same webhook_secret — and the header Idempotent-Replayed: true, so a timed-out POST can simply be sent again without queueing the list twice. The same key with a different body answers 409 IDEMPOTENCY_KEY_REUSED rather than the wrong job. The request is fingerprinted as the lookup will read it — the list-level country already folded into each item — so a retry that spells the same request differently still replays.
Poll GET /api/v1/address/bulk/{jobId} for the status, processed, duplicate_count, the running valid and invalid counts, the low, medium and high risk counts and credits_used. Polling is free. POST /api/v1/address/bulk/{jobId}/cancel stops a queued run on the spot and a running one at its next checkpoint; the rows already answered stay billed and downloadable, and a job that has already finished answers 409 JOB_ALREADY_FINISHED.
A run that stopped because your credits ran out is resumed rather than uploaded again: top up, then press Resume on the run or call POST /api/v1/address/bulk/{jobId}/resume. The whole list runs again from the top, and the rows it already answered are repeats inside the seven-day window, so only the rows it never reached are charged. The job keeps its id, its webhook and its cumulative credits_used, while processed and the verdict counts restart at zero and are rebuilt as the rows come back. A run that stopped for any other reason, or that is already going again, answers 409 JOB_NOT_RESUMABLE; one whose list has passed out of the 30-day retention window answers 409 JOB_INPUT_EXPIRED, and an empty balance answers 402 INSUFFICIENT_CREDITS rather than queueing a run that would stop again at once.
If a delivery fails — your endpoint was down for the half-minute the three attempts cover — ask for it again rather than falling back to polling: press Send webhook again on the run in the dashboard, or call POST /api/v1/address/bulk/{jobId}/webhook/redeliver. The body is byte-for-byte the body the first delivery carried, rebuilt from the run's own frozen row, so the two reconcile. The signature is new, and has to be: a receiver refuses anything whose t is more than five minutes from its own clock, so verify a redelivery exactly as you verify any other delivery. Nothing deduplicates it for you — a receiver that gets both the original and the redelivery sees bulk_address_job.finished twice, and job.id is the key to deduplicate on. A run created without a webhook_url answers 409 WEBHOOK_NOT_CONFIGURED and one that has not settled answers 409 JOB_NOT_FINISHED; a run whose last delivery succeeded is not refused, because a receiver can lose a message it has already acknowledged. The delivery is queued rather than made while you wait — one attempt takes half a minute against an endpoint that is down, which is the endpoint you are asking about — so the call answers 202 and the run reads webhook_status: "pending" until a worker has an answer, then delivered or failed with what your endpoint said in webhook_detail. The dashboard shows the same three states beside the run. It is free and capped at ten a minute counted per signed-in user: neither per key nor per account, so two keys held by one person share the one budget while two teammates have one each.
GET /api/v1/address/bulk/{jobId}/results streams the CSV. ?variant= narrows the rows to the ones that stand as written or the ones that do not (valid, invalid), or to one risk level (low, medium, high); full is the default. A job that is still running answers 409 RESULTS_NOT_READY, one cancelled before any row was processed answers 409 JOB_CANCELLED, and one that failed before writing a row answers 409 JOB_FAILED; neither of those last two billed anything. A run cancelled or failed part-way keeps the rows it did write, and they stay downloadable.
The CSV leads with the uploaded file’s own columns, unchanged and in their original order, so the results line up with the file you sent. Then address — the address exactly as it was submitted, so a pasted list has something to join on — then every response field in the order the API answers them, then sources and credits_used and suppressed — the last of those true on a row answered from your suppression list rather than read again:
address, valid, reason, country, country_name, continent, is_eu, is_eea, privacy_regime, formatted, formatted_lines, formatted_local, organization, building, street, house_number, unit, po_box, dependent_locality, locality, administrative_area, postal_code, postal_code_formatted, postal_code_valid, postal_code_type, postcode_status, locality_matches_postcode, administrative_area_matches_postcode, address_type, address_type_source, is_po_box, is_private_mailbox, exists, match_level, register, register_coverage, premise_key, building_use, is_multi_unit, unit_check, street_suggestion, latitude, longitude, location_source, companies_registered, is_mail_drop, mail_drop_provider, mail_drop_source, deliverability_checked, is_deliverable, is_cmra, is_vacant, is_residential, confirmation, changes, looks_placeholder, risk_score, risk_level, risk_signals, ok_to_ship, blocked_by, sources, credits_used, suppressed
The array fields are flattened rather than dropped: sources as dataset@version and risk_signals as signal:weight, both joined with ;. The multi-line formatted address is written on one line, its lines joined with a comma and a space, and any cell containing a space or a comma is quoted. An empty cell is a null, and a null means the field was not evaluated — never "no".
When the run ends, whatever the outcome, the finish is POSTed to webhook_url as bulk_address_job.finished:
{
"event": "bulk_address_job.finished",
"job": {
"id": 91,
"status": "completed",
"total": 2,
"processed": 2,
"valid": 1,
"invalid": 1,
"low": 1,
"medium": 0,
"high": 0,
"credits_used": 1,
"stopped_reason": null,
"finished_at": "2026-09-05T12:04:11+00:00"
}
} The X-Spaw-Signature-V2 header carries t={unix seconds},v2={lowercase hex}, where the hex is the HMAC-SHA256 of v2:{t}:{raw body}, keyed with the webhook_secret creation returned once. Verify it against the exact bytes you received — not against a re-encoded copy of the parsed JSON — compare with a constant-time function, and refuse anything whose t is more than five minutes from your own clock: without that check a captured delivery can be replayed at you forever. The original X-Spaw-Signature, the HMAC of the body alone, is sent beside it until 1 March 2027 and still verifies. A cancelled run fires the same webhook with status: "cancelled", and a delivery that fails is retried twice with a backoff, three attempts in all.
Billing is per row and identical to a single lookup: a row that answers valid: true costs one credit, a row that cannot stand costs nothing whatever the reason, and a row repeating an address you looked up in the last seven days is free. Rows repeating an address already in the same list are answered once, cost nothing, and are counted in the job’s duplicate_count. Deliverability adds its premium only to the rows a partner actually answered. If the balance runs out part-way, the run stops with stopped_reason: "insufficient_credits" and the rows already paid for stay downloadable.
The address verification page in the dashboard runs the same jobs from a Bulk runs tab: upload a CSV, or paste one written address per line, pick the country for the rows that do not name their own, and follow the run's progress there. Its valid, invalid and risk counts fill in when it finishes. An uploaded file keeps every column, and its headers are mapped to the address fields — address, street, city, postcode, country and the rest — so a spreadsheet of separated fields works as well as a column of written lines.
Monitored address lists
An address is not a fact that stays true. A postal operator terminates a postcode, a building leaves the national register, a suite quietly becomes a branch on a mailbox operator’s published list. A monitored list ( POST /api/v1/address/monitors) keeps up to 500 addresses — your own sites, a supplier’s registered offices, the delivery addresses behind an order book — and re-reads all of them on a schedule.
cadence is weekly or monthly. There is deliberately no daily option: every dataset an address is measured against — the national address registers, the postcode directories, the open company registers — is republished weekly at best, so a daily run would charge you for reading the same files again.
{
"name": "Supplier sites",
"cadence": "monthly",
"country": "GB",
"addresses": [
"10 Downing Street, London SW1A 2AA",
{
"address_line1": "Postfach 10 01 01",
"postal_code": "50441",
"locality": "Köln",
"country": "DE"
}
]
} Each item is a written address or an object of the separated fields and may carry its own country; the list-level country applies to every item that does not. deliverability: true asks the licensed partner on every run. Creation answers 201 and queues the baseline run straight away.
What a run says about each address
GET /api/v1/address/monitors/{monitorId} answers results: the last run’s verdict per address, keyed by the address as you saved it, with the status below, the flags that fired, and whether it changed since the run before. One address can match several conditions — a terminated postcode at an address the register also dropped — so flags lists them all and the status is the first of them.
| ok | The address still stands as written, and nothing was flagged. |
| invalid | It no longer stands as written; the row’s reason says which part failed. |
| not_in_register | A register whose coverage is complete no longer carries the address. |
| terminated_postcode | The postal operator retired the postcode. |
| mail_drop | The address now looks like a mail drop. |
| high_risk | The published weights now add up to a high risk_score. |
| unverifiable | The address stands, but nothing installed here could check it. |
unverifiable is the honest answer for a country whose datasets this server does not hold. The address parsed and formatted correctly against its own country’s rules, and then there was nothing to compare it with: no address register, no postcode directory and no company register covering it. It is counted apart from ok so you can see how much of a list nobody here can check, it says something about the data installed on this server and never about the address, and it never raises an alert. A list of addresses in a country with no installed dataset reports unverifiable for every one of them rather than pretending to have looked.
not_in_register fires only where exists is false, and exists is false only when the register that answered has complete coverage and does not carry the address. A register whose coverage is partial — the US National Address Database and the French Base Adresse Nationale among them — answers exists: null for an address it does not carry, and null means the address was not evaluated, never that it does not exist. So a partial register can never move an address into not_in_register, and neither can a country with no register at all.
mail_drop is a heuristic, not an official flag: it fires when a mailbox operator’s own published branch list names the address, when a registered-agent or formation service publishes the address as its own or a public listing or filing naming the service carries it, or when so many companies are registered at it that it is one in practice, and mail_drop_source says which of the three. It is never a postal service’s own agency flag, and it is a statement about a place, not about anyone who receives post there.
Who gets told, and when
An email goes out only when an address that was already known and not flagged becomes flagged. A first run sets the baseline and mails nothing, however the list reads; an address you add to an existing list is baselined the same way on its own first run; and an address that stays flagged run after run never alerts again. The mail names each newly flagged address and why: the reason it stopped standing, that the register no longer carries it, the day the postcode was terminated where the directory records one, the operator whose branch list names it or the number of companies registered there, or the risk score.
last_summary counts every status — total, ok, invalid, not_in_register, terminated_postcode, mail_drop, high_risk and unverifiable — alongside changed, newly_flagged (the addresses that email named), credits_used and stopped_reason.
Billing, and the rest of the endpoints
Each run bills every address exactly like a single lookup: one credit for an address that answers valid: true, nothing for one that cannot stand whatever the reason, and nothing for a repeat of the same address within seven days. Runs are at least a week apart, so that marker has normally expired and each scheduled run pays for a fresh answer; a run you trigger by hand right after another is free for the same reason. deliverability: true adds its premium only to the addresses a partner actually answered, and adds nothing while no partner is configured. If the balance runs out part-way the run stops with stopped_reason: "insufficient_credits", and the addresses already re-read keep their new status.
GET /api/v1/address/monitors lists the account’s monitors, newest first, 100 per page, each with the summary of its last run. POST /api/v1/address/monitors/{monitorId}/run re-checks a list ahead of schedule and answers 202. DELETE /api/v1/address/monitors/{monitorId} stops it and removes its stored results. A monitor that belongs to another account answers 404.
A monitor is the one place an address is kept: the list you saved and the last run’s status per address stay in your account until you delete the monitor, because there is no other way to tell you what changed. Nothing else about those lookups is stored.
The address verification page in the dashboard runs the same lists from a Monitors tab: paste up to 500 written addresses, one per line, name the list, choose weekly or monthly and the country they are read against, and each row then shows the address count, the last run with anything flagged in red and the unverifiable count beside it, the next run, and buttons to run it now or remove it.
A monitor can post its runs as well as email about them. Set webhook_url (https) when you create it, or later with a PATCH, and the signing secret comes back once in that response and is never readable again. webhook_events decides which runs are posted: changes, the default, posts only when the run found something new — an address that is newly flagged, or one whose status changed — or stopped early, and every_run posts after every run, which also tells you the monitor is still running. The body is { "event": "address_monitor.run", "monitor": {…}, "summary": {…}, "changes": [ … ] }, where changes names the addresses the alert email names, and X-Spaw-Signature-V2 carries t={unix seconds},v2={hex}, where the hex is the HMAC-SHA256 of v2:{t}:{raw body} keyed with the secret. Refuse anything whose t is more than five minutes from your own clock: that window is what stops a captured delivery being replayed at you later. The original X-Spaw-Signature, the HMAC of the body alone, is sent beside it until 1 March 2027 and still verifies. Connection errors and 5xx answers are retried twice, three attempts in all; webhook_status and webhook_detail on the monitor say how the last delivery went. A URL that resolves onto a private network is refused at delivery, every time, because DNS can be repointed after a URL is saved.
A monitor is changed rather than replaced: PATCH /api/v1/address/monitors/{id} takes name, addresses, cadence, country, deliverability and the webhook fields, and applies only the ones you send. That matters because what the last run recorded per entry is the baseline every alert is compared against: deleting a monitor and creating it again throws that away, and the first run of the new one only sets a baseline — a full cycle of silence, which is the one thing a monitor is bought to prevent. Replacing the list keeps what was recorded about the addresses that stayed, deletes what was recorded about the ones you removed, and gives a new entry no baseline. Changing the cadence neither skips a run nor starts a second one: a run already scheduled sooner than the new interval still happens, and a longer interval applies from the one after it. A body naming no field it could change answers 422.
A monitor that stops being run is deleted, and the identifiers and results it stored go with it. prunes_after_days on every monitor payload says how long that takes. A monitor in active use is never pruned: each run advances last_run_at, so the window only closes on a list that has genuinely stopped being re-checked — at which point holding the details somebody gave you serves no purpose. Deleting a monitor yourself removes everything it held at once.
Outcome reporting and the suppression list
A lookup can only say what open data knows. What the post office tells you afterwards is better evidence than any register, and it can be sent back. POST /api/v1/address/feedback takes up to 1,000 items, each an address — written on one line as address, or as the separated fields, with an optional own country — an outcome, and an optional occurred_at. The list-level country applies to the items that do not name their own. It answers 202 with {"recorded", "skipped"}, costs nothing, and is never logged as a lookup.
$ curl https://spaw.co/api/v1/address/feedback \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"country": "GB", "items": [
{"address": "10 Downing Street, London SW1A 2AA", "outcome": "returned", "occurred_at": "2026-09-01"},
{"address_line1": "Postfach 10 01 01", "country": "DE", "outcome": "moved"}
]}'There are four outcomes, and they describe what happened to post you actually sent:
| delivered | The post arrived. | Clears an entry the feedback put there. An entry you imported is left alone. |
| returned | The item came back undelivered. | Suppresses on the second such report that is not followed by a later delivered. |
| moved | The occupant has moved on. | Suppresses at once. |
| refused | Delivery was refused at the door. | Suppresses at once. |
GET /api/v1/address/feedback/summary answers the account’s own counts by outcome over the reporting window.
Your reports are never shared with another account
This is a deliberate decision and not an oversight. The IP and phone products both have a cross-account signal: enough separate accounts reporting one IP address raises everyone’s risk on it, and the phone product reads the regulator’s own tables. Address feedback has no equivalent and never will. An IP address is a network endpoint and a phone number is a handset, but a postal address is where somebody lives, and a shared “post fails here” database would be a database about residences, assembled from other people’s post. Your reports raise the risk of your own later lookups of an address and nobody else’s, and nobody else’s reports ever reach yours.
That is what reported_returned (+50, dataset address-feedback) is: it fires when the calling account’s own most recent report for the address was returned, moved or refused rather than delivered. A later delivered report cancels it. The same address looked up by a different account carries no such signal, because that account has reported nothing about it.
The suppression list
An address that earns its way onto the list is answered from the list instead of being read again. GET /api/v1/address/suppressions lists the account’s entries newest first, 100 per page, each with the address as it was submitted, the reason (returned, moved, refused, or imported for a list the owner uploaded), the source and when it was added. POST /api/v1/address/suppressions imports up to 10,000 addresses in one call, free. DELETE /api/v1/address/suppressions/{suppressionId} removes one entry. An entry you imported is never overwritten or cleared by your reports or a lookup: it is your do-not-send list, not ours, and it leaves only when you remove it. Importing an address that is already listed makes it one you imported, keeping its reason.
Finding one address again does not mean paging the list. The Suppressions tab has a search box, and the list endpoint takes value with its country, plus source, reason, since, until and per_page. A searched address is folded against that country's postal format before it is matched, so any spelling of a suppressed address finds it. To drop addresses without looking up their ids, DELETE /api/v1/address/suppressions takes up to 1,000 of them in values with a country, and answers which were on the list and which were not.
Four rules keep the list honest:
- Batch, bulk and monitor runs honour it. A suppressed address is answered from the list without being read again, free, with
meta.suppressed: trueon the item. - A single lookup never honours it.
POST /api/v1/addressalways reads the address fresh and bills it normally, because a new occupant is exactly the case a cached “post fails here” would get wrong. It never takes off an entry your reports still stand behind, or an import: an entry your reports added leaves when you report a delivery, or at the next single lookup once those reports have aged out of the 90-day window, and an import only when you remove it. - An entry your reports added is re-verified rather than served once it is older than 90 days. Nothing your reports put on the list is trusted forever; people move in as well as out. A stale entry is kept while the reports still stand behind it, and a new moved, refused or returned report renews it. An import never goes stale: it is your own list, served for as long as it stays on it.
- Reporting and importing are free. Neither endpoint costs a credit, and neither is logged as a lookup.
The address verification page in the dashboard has a Suppressions tab over the same list: paste addresses one per line to import them, name the country they are all read against, export the whole list as CSV, and remove an entry with one button. Each row shows the address, the reason in words — returned twice, occupant moved, delivery refused, or imported — the source and when it was added.
Billing and privacy
A fresh lookup that answers valid: true costs one credit. An address that cannot stand as written costs nothing, whatever the reason. A repeat within seven days is free, and the repeat marker is keyed on the address as the pipeline folds it — case, punctuation, abbreviations and spacing already folded away — so two spellings of one address are one lookup and one charge.
Asking for deliverability: true reserves the premium credits up front, so a request that cannot afford them is refused cleanly, and charges them only when the partner actually answered. While no partner is configured the check answers deliverability_checked: false and adds nothing to the bill. Its answers are cached apart from the plain lookup, like any premium answer.
A postal address is personal data, and nothing about an address lookup is stored. No history row, no log line with the address. The traces are the keyed seven-day marker that makes a repeat free and the count behind seen_by_you, kept under the building's digest; both are hashes, never the address. Every signal is a local table or a bundled file, so no third party sees the address — unless you ask for the deliverability check, which is the one call that leaves the server, and only for the address you asked about.
A bulk run is the one exception, and only for as long as it needs to be: the list you submitted and the result CSV are written to disk because there is no other way to process a job in the background and hand the answers back. They are the only copies that exist, and both are deleted along with the job after 30 days. A monitored list is the other: the addresses you saved and the last run’s status for each of them stay in your account until you delete the monitor.
Outcome reports are the third: an address you report on is kept with the outcome and its date, and an address on your suppression list is kept until you remove it or, for one your reports added, until they no longer stand behind it, because a list you cannot see is not a list you can trust. Both live inside your account, are readable and deletable there, and are never read by another account.
The privacy policy states the same facts alongside the rest of the data handling.
Next: the API reference for the envelope, the error codes and every endpoint.
Guides
All guides- How to tell a UK virtual-office or formation address from a trading address
- A suite at a single-family home
- Gate a checkout with the shipping policy
- Placeholder addresses: what people type instead of an address
- Reading a British postcode district page
- What a postcode directory learns from a register
- A registered agent's address is not a mail drop, and not a home
- How an open register locates a building
- How to read the address confirmation map
- Number ranges: what the US range match can and cannot say
- What a street match means in Great Britain
- Can you look up UK addresses from a postcode for free?
- How to read an address-existence answer
- What a PMB (private mailbox) is, and why it cannot prove where you live