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.
Fields and flags
Every response carries the same 45 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. |
| organization | The company line, as sent or as recognised in a written address. |
| 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 or mailbox line. A private-mailbox number written on the street line is lifted out into this field. |
| po_box | The box line in the operator’s own words (PO Box 1234, Postfach 10 01 01, BP 12). A box written on the street line moves here and the street line is cleared, because the box replaces it. |
| 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. |
| 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 when no directory is installed for the country. |
| locality_matches_postcode | Whether the town on the address is the town the directory publishes for that postcode, compared on folded names. Null when either side is unknown, which in Great Britain is always: the ONS directory publishes no post town. |
| 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. |
| 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 the address is written the way the postal service requires a box at a mail-receiving agency to be written — a PMB or # number after a real street address — or when the licensed check reported the delivery point as an agency. |
| exists | True only when the building itself was found in the country’s official address register. False only when a register whose publisher claims to list every address does not carry it. Null in every other case: no register for the country, a box with no street behind it, or a register that covers only part of the country. |
| match_level | How far the address could be followed into that register: to the building, to the street, to the postcode, or not at all. Null when no register was consulted. |
| 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. Always read exists against this field. |
| latitude / longitude | The centroid the postcode directory publishes for the postcode. Not a rooftop, and not derived from the street or the house number. |
| location_source | postcode_centroid whenever coordinates are given, so nothing about their precision has to be guessed. Null when there are none. |
| companies_registered | How many companies name this address in an open company register. Null where no register covers the country. |
| is_mail_drop | Whether mail is received at this address on other people’s behalf. True from either of the two sources below; false when at least one of them was consulted and neither matched; null when neither was. |
| mail_drop_provider | The operator that publishes this address as one of its branches. Null when the flag came from the company count instead. |
| mail_drop_source | Which of the two answered: the operator’s own published branch list, or more companies registered at the address than the published threshold. |
| 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. |
| risk_score / risk_level | The sum of the weights below, capped at 100; high from 60, medium from 30. Null for an invalid address. |
| risk_signals | Each contributing signal with the weight applied and the dataset behind it; the weights sum to risk_score. |
| 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 replaces the street line rather than sitting beside it, so a recognised box line clears street and house_number and lands in po_box. A private mailbox is the exception: it stands at a real street address, so the street stays and the mailbox number moves into unit.
Registers, coverage, and what exists means
Six countries have an official address register installed, and each publisher says how complete its own file is. 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.
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. Absence from a partial register is not evidence that an address does not exist, and it is never treated as such. Switzerland, Austria, Czechia and Norway publish complete registers, and there absence is an answer.
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. 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 |
| 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 |
| 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 | +30 |
| not_in_register | Not in a register whose publisher lists every address | +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 that claims complete coverage, 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 206 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. 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. |
| fr-base-adresse-nationale | exists and match_level in France and its overseas regions. Coverage is partial. |
| ch-gebaeudeadressverzeichnis | exists and match_level in Switzerland and Liechtenstein, from the official directory of building addresses. Coverage is complete. |
| at-adressregister | exists and match_level in Austria, from the address register of the Bundesamt für Eich- und Vermessungswesen. Coverage is complete. |
| cz-ruian-adresni-mista | exists and match_level in Czechia, from the RÚIAN address points of the Czech Office for Surveying, Mapping and Cadastre. Coverage is complete. |
| no-matrikkelen-adresse | exists and match_level in Norway, from Matrikkelen, the national property register kept by Kartverket. 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. It publishes no post town, so locality_matches_postcode stays null for British addresses. Northern Ireland postcodes are excluded: reusing them needs a separate Land & Property Services licence, so a BT postcode answers as if no directory were installed. |
| uk-companies-house | companies_registered in the United Kingdom, from the registered office of every company in the free company data product. |
| company-registers (US) | companies_registered in the United States, from the Colorado, New York and Oregon business registries — principal and registered-agent addresses. |
| mail-drop-locations | is_mail_drop and mail_drop_provider from the branch addresses mailbox and mail-forwarding operators publish themselves. |
What the lookup deliberately does not claim: it does not know whether post is actually delivered to an address; it cannot list the addresses at a UK postcode; a partial register cannot say an address does not exist; postcode coordinates are a centroid and not a rooftop; and nothing anywhere says who lives or works there. Each of those is a null or a typed refusal, never a guess.
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. 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.
GET /api/v1/address/postcode/{postcode}?country=GB lists the addresses at a postcode. It answers 501 ADDRESS_LIST_UNAVAILABLE while no licensed partner is configured, and nothing is charged: every full UK address list derives from the Royal Mail Postcode Address File, which no open dataset replaces. The endpoint and its shape exist now so nothing changes for your client when one is signed.
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.
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.
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, and one cancelled before any row was processed answers 409 JOB_CANCELLED.
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, organization, street, house_number, unit, po_box, dependent_locality, locality, administrative_area, postal_code, postal_code_valid, postal_code_type, postcode_status, locality_matches_postcode, address_type, address_type_source, is_po_box, is_private_mailbox, exists, match_level, register, register_coverage, 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, risk_score, risk_level, risk_signals, 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 header is the HMAC-SHA256 of the raw request body, keyed with the webhook_secret creation returned once, as lowercase hex. Verify it against the exact bytes you received — not against a re-encoded copy of the parsed JSON — and compare with a constant-time function. A cancelled run fires the same webhook with status: "cancelled", and a delivery that fails is retried three times with a backoff.
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 watch the counts move. 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 that publishes complete coverage of its country 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 publishes complete coverage of its country and does not carry the address. A register covering part of a country — the US National Address Database and the French Base Adresse Nationale both do — answers exists: null, 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, or when so many companies are registered at it that it is one in practice, and mail_drop_source says which of the two. 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.
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 by an automatic one: it is your do-not-send list, not ours.
Four rules keep the list honest, and they are the same four the email and phone lists follow:
- 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. That is the whole escape hatch: a fresh single lookup is how an address earns its way off the list, and a new occupant is exactly the case a cached “post fails here” would get wrong. - An entry older than 90 days is re-verified rather than served. Nothing on the list is trusted forever; people move in as well as out.
- 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 Suppressed 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 it is treated the way a phone number and an IP address are: nothing about an address lookup is stored. No history row, no log line with the address. The only trace is the keyed seven-day marker that makes a repeat free, and it is a hash. 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, 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.