n8n runs on n8n Cloud or on your own server, and either way its HTTP Request node can talk to Spaw with nothing installed. A community node, n8n-nodes-spaw, is built and will be published to npm; it wraps the same endpoints as resources and operations with a saved credential. Both paths are described here.
What works today: the HTTP Request node
- Create a secret API key on the API keys page of your Spaw dashboard.
- In n8n, create a credential of the generic Header Auth type. Set the name to
Authorizationand the value toBearer sk_live_…. The key is stored encrypted and never shows in the workflow JSON. - Add an HTTP Request node after the node that produces the address. Set the method to POST and the URL to
https://spaw.co/api/v1/email. - Set authentication to the generic credential type, pick Header Auth, and choose the credential from step 2.
- Turn on sending a body, choose JSON, and use an expression for the content:
{
"email": "{{ $json.email }}"
}
- Execute the node once with
[email protected]in the incoming item. It is one of six free test addresses that answer a fixed result and never touch your history.
The node outputs the API's envelope as one item: data holds every field of the single-address response, meta holds credits_used, credits_remaining, cache_hit and request_id.
For many items at once, send them to the batch endpoint instead. Set the URL to https://spaw.co/api/v1/email/batch, enable the node's option to execute once, and build the body with an expression that gathers every incoming item:
{{ { "emails": $input.all().map(item => item.json.email).slice(0, 50) } }}
The answer's data.results array is in input order; a Split Out node turns it back into one item per address. A batch counts as one request against the 5 requests per second limit, which matters for workflows fed by large lists.
The community node
n8n-nodes-spaw is a declarative community node written in TypeScript with no runtime dependencies, which is what n8n asks of nodes it verifies. Once it is on npm, self-hosted instances install it from the community nodes settings by package name, and n8n Cloud shows it after n8n's verification. It offers:
- A Spaw API credential holding the secret key, tested against your account endpoint when saved.
- Email resource: Verify, and Verify Many for up to 50 addresses per call.
- Domain resource: Get, the same signals as the domain endpoint.
- Suppression resource: Add, Get Many, and Remove, so bounce lists from other systems can be kept in sync.
- IP resource: Look Up, and Look Up Many for up to 1,000 addresses per call, both with a Privacy Mode option.
- Address resource: Validate, with the address on one line, a required Country, and an Options collection for the parts you would rather send separately (a named Building among them) and for the licensed deliverability check. The item carries every field of the answer,
confirmation(what stood behind each part) andchanges(what the parser set aside on the way) included. - Entity resource: Verify, with an Identifier, an Identifier Type of LEI or Company Number, and a Country field that appears for a company number, which needs the register it belongs to.
- Phone resource: Validate, Validate Many for up to 1,000 numbers per call with a shared Country and a Live Carrier Check option, and Report Outcome to tell Spaw what happened after an SMS (delivered, undelivered, abused, verified or opted out), which is free. The IP resource has its own Report Outcome (abuse, fraud, bot or chargeback), free as well: your own reports score your own later lookups of the address.
It is not published yet. The HTTP Request node above uses the same endpoints, so a workflow built today can swap nodes later without changing what is stored.
IP lookups
The HTTP Request node looks up addresses the same way: method POST, URL https://spaw.co/api/v1/ip, the same Header Auth credential, and a JSON body of { "ip": "{{ $json.ip }}" } mapped from whatever recorded the visitor's address. The answer's data block carries the location, the network and its operator type, tri-state flags for datacenter and cloud ranges, Tor exits, VPN exits and iCloud Private Relay with the provider named when its own list contains the address, is_anonymous, and a published risk_score with its risk_signals. An IF node on {{ $json.data.is_anonymous }} or a Switch on {{ $json.data.risk_level }} covers most screening rules. For lists, https://spaw.co/api/v1/ip/batch takes { "ips": [...] } with up to 1,000 addresses per call; in the community node these are the IP resource's Look Up and Look Up Many operations, whose Privacy Mode option keeps nothing about the lookup on Spaw's side. One credit covers five IP lookups that resolve a location, a network or a positive flag: the lookup that opens a block reports credits_used 1, the next four 0, and meta.ip_lookups_prepaid counts what is left. Reserved or private ranges, unallocated space, unknown addresses and repeats within seven days are free, and IP endpoints have their own limit of 50 requests per second.
Phone numbers
The HTTP Request node validates a phone number the same way: method POST, URL https://spaw.co/api/v1/phone, the same Header Auth credential, and a JSON body of { "phone": "{{ $json.phone }}" }, with a country key (an ISO 3166-1 alpha-2 code such as US) when the numbers are written without a calling code. The answer's data block carries valid and a typed reason, the E.164, national, international and tel: forms, the country and its facts, line_type and carrier with the source that settled them (the numbering plan, or the block allocation published by the regulator, which says who the numbers were allocated to and never who serves them after a port), block_status, the is_fictional, is_virtual and is_disposable flags, and a published risk_score with its risk_signals. An IF node on {{ $json.data.valid }} gates the signup; a Switch on {{ $json.data.risk_level }} or {{ $json.data.is_disposable }} separates numbers worth an SMS from numbers that only receive codes for strangers. An IF node on {{ $json.data.ok_to_send }} is the shorter gate: it is your account's SMS policy applied to this answer (the countries and line types you send to, the signals you refuse, a ceiling on the score and, by default, the numbers your suppression list holds as opted out), and on the false branch {{ $json.data.blocked_by }} names the rule that decided it (invalid, opted_out, country, line_type, a signal such as disposable, or risk_score; suppressed for a number on your suppression list, which batch, bulk and monitored runs honour and a single lookup never does, while a single lookup of a number the list holds as opted out answers opted_out unless the policy's block_opted_out is off). The policy is set once on the account, not in the workflow, so every workflow, batch and bulk run applies the same rules. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. In the community node this is the Phone resource's Validate operation. A phone lookup costs 1 credit when the number is valid; invalid numbers, unassigned blocks and repeats within seven days are free.
For a list, point the same node at https://spaw.co/api/v1/phone/batch with { "phones": [...] } (up to 1,000 numbers, each a string or { "phone", "country" }) and an optional list-level country; the answer's data.results is in input order and numbers on your suppression list come back free. In the community node this is Validate Many, whose Options collection carries Live Carrier Check (the API's hlr). The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. After you send an SMS, tell Spaw what happened with POST https://spaw.co/api/v1/phone/feedback and { "items": [{ "phone": "...", "outcome": "delivered" }] } (undelivered, abused, verified and opted_out are the other outcomes): the report is free, feeds only your own later lookups of that number and your own phone suppression list, and is the Phone resource's Report Outcome operation in the community node. An undelivered item can carry the failure_class your provider's error code names (sender_side for a carrier filter or an unregistered campaign, permanent for a number that no longer exists), and only failures about the number count against it.
Postal addresses
The HTTP Request node checks a postal address the same way: method POST, URL https://spaw.co/api/v1/address, the same Header Auth credential, and a JSON body of { "address": "{{ $json.address }}", "country": "GB" }, or the parts separately as address_line1, locality, administrative_area, postal_code and the rest. The country is not optional in practice: it decides which format the address is read under, which parts that country requires and which postcode directory answers, and it is never guessed. The answer's data block carries the components and the formatted address, postal_code_valid and postcode_status, whether the town matches the postcode, address_type with the source that settled it, match_level against the register named, the postcode centroid, companies_registered, and a published risk_score with its risk_signals. An IF node on {{ $json.data.ok_to_ship }} gates the order: it is your account's shipping policy applied to this answer, and on the false branch {{ $json.data.blocked_by }} names the rule that decided it (invalid, a signal such as placeholder, risk_score or unconfirmed; suppressed for an address on your suppression list, which batch, bulk and monitored runs honour and a single lookup never does), the reason to write back to the order or pass to whoever reviews it. The policy is set once on the account, not in the workflow, so every workflow, batch and bulk run applies the same rules; a Switch on {{ $json.data.address_type }} is still there when a workflow needs to route a PO box or a private mailbox differently rather than refuse it. In the community node this is the Address resource's Validate operation; for a list, https://spaw.co/api/v1/address/batch takes { "addresses": [...] } with up to 100 per call.
An empty field means the signal was not evaluated, never "no". exists is true only on a premise match and false only under a register whose coverage is complete; a register whose coverage is partial, because it covers part of the country or cannot vouch for every address, cannot make an address's absence mean anything. is_mail_drop is a heuristic and not a postal service's own agency flag: an operator's own published branch list names the address, or so many companies are registered there that it is one in practice, and mail_drop_source says which. Two answers are licensed and not enabled — whether post actually arrives (deliverability_checked comes back false with is_deliverable, is_cmra, is_vacant and is_residential empty) and the list of addresses inside a postcode, which answers a typed 501 rather than a guess. An address lookup costs 1 credit when the address is valid; invalid addresses and repeats within seven days are free.
Business identifiers
The HTTP Request node checks a company the same way: method POST, URL https://spaw.co/api/v1/entity, the same Header Auth credential, and a JSON body of { "identifier": "{{ $json.lei }}", "type": "lei" } — or { "identifier": "{{ $json.company_number }}", "type": "company_number", "country": "GB" }, because a company number is only unique inside its own register and the country says which one, while a LEI is global by construction and ignores it. The answer's data block carries the legal name, status with the register's own status_detail, the country and jurisdiction of formation, the category and legal form, registered_at and record_updated_at, and for a LEI checksum_valid, registration_status and next_renewal_at. Where a LEI names its entry in a national register Spaw also holds, agreements quotes both sides field by field and flags names what differs. An IF node on {{ $json.data.found }} separates identifiers no register carried from the rest; a Switch on {{ $json.data.status }} or a filter on dissolved_in_national_register in flags stops a workflow before it signs with a company that no longer exists. In the community node this is the Entity resource's Verify operation, whose Country field only appears once Company Number is picked. For a list, https://spaw.co/api/v1/entity/batch takes { "identifiers": [...] } with up to 500 per call, each a LEI string or an object of identifier, type and country.
The one caveat, and it matters more than anything else here: the UK company file is a snapshot of the live register, so a company that is struck off is removed from the file rather than marked dissolved — the statuses that file carries are Active, Active - Proposal to Strike off, Liquidation, In Administration and similar, and never Dissolved. A UK dissolution therefore reaches you as found false with reason unknown_identifier rather than as that flag, which never fires on the only national register installed today. Check found before you trust the flag, and read status_detail for the earlier warning: Active - Proposal to Strike off folds to a status of active, because the company is still on the register. A LEI lookup of the same company is quieter still: GLEIF keeps its record, so found stays true and no flag fires. What moves is agreements, which comes back empty while other_register still names uk-companies-house — the counterpart row is gone. That pair is the signal on this path, and lei_registration_not_current follows later, when the lapsed registration is not renewed. A monitored list turns the disappearance into an alert of its own, left_register.
checksum_valid is a typing check and nothing more: a perfectly formed LEI can never have been issued, which is what the register lookup answers. registration_status is about the identifier rather than the entity — a LEI must be reconfirmed each year and a great many are not, so LAPSED means the reference data has not been rechecked recently, not that the company has gone. Nothing in the answer is a registered address, and nothing will be: a registered address is very often somebody's home, and an identifier alone does not say whether a company or a sole trader stands behind it.
Both registers are bulk files synced onto the Spaw server, so no third party sits in the request path. The LEI reference data is published under a CC0 public-domain dedication and refreshed daily; the UK company data is used under the Open Government Licence v3.0 and republished monthly, so an answer can be up to five weeks behind the live register and every answer names the date of the copy in sources. Spaw is not affiliated with, endorsed by or connected to either publisher. A business lookup costs 1 credit when a register carried the identifier; unknown, malformed and unsupported identifiers and repeats within seven days are free.
What to do with the verdict
Add an IF node after the request and compare {{ $json.data.deliverable }} with deliverable. The true branch continues to your CRM or mailing tool. On the false branch, a Switch node on {{ $json.data.reason }} separates catch_all and role, which are often worth keeping, from disposable and mailbox_not_found, which are not. When {{ $json.data.did_you_mean }} is filled, the address was a near miss, and writing the suggestion back to the source recovers the record. For a numeric threshold use data.risk_score, whose weights are published.
Workflows that verify at scale should also read the suppression list occasionally through the list endpoint: every undeliverable verdict lands there, and batch and bulk runs answer suppressed addresses for free.
Cost
A fresh deliverable or risky answer spends 1 credit; unverified answers, invalid input, repeats within seven days and the test addresses are free, and undeliverable answers are free within a fair-use allowance. n8n executions are counted by n8n separately. The node handles 429 RATE_LIMITED like any HTTP error, so enable the HTTP Request node's retry option for bursty workflows.