- authentication
- Secret API key as a bearer token
- billing
- Each signal bills exactly as its own single-lookup endpoint: 1 credit per definitive answer, free when the answer is no, and free for a 7-day repeat of the same input on the same account. `meta.credits_used` is the sum, and the consistency analysis itself is free. A call that runs out of credits part-way keeps the signals it paid for and reports `meta.stopped_reason`; only a call refused on its first signal answers 402.
Takes any subset of an email address, a phone number, an IP address, a postal address and a business identifier, runs each through the product that owns it, and reports where the answers agree and where they do not.
The per-signal blocks under signals are exactly what the single-lookup endpoints return, field for field. All five keys are always present: a signal you did not ask for, or one the call did not reach, is null. The signals run in a fixed order — ip, phone, entity, address, email — because a keyed body has no input order, and because email is the only leg that does DNS and a mailbox probe, so a call that runs out of credits stops at the slowest leg last.
The consistency block on top is free. comparisons lists each check with both sides quoted — the signal, the field and the value that decided it — so nothing is asserted that you cannot see the working for. A comparison appears only when both of its signals answered; a signal nobody asked about produces silence, not a row. Where one side is null the result is not_compared with withheld: "missing_value", never a disagreement, because a null field means "not evaluated" and never "no". Where the IP is a VPN exit, a relay or a datacenter egress, the country comparisons answer not_compared with withheld: "low_confidence": that location belongs to the operator rather than to the person, and reporting a difference there would manufacture evidence against everyone who uses a VPN.
country_agreement rolls up the three country comparisons between the person's own signals — differ if any of them differed, agree if at least one agreed and none differed, and null when none of them could be made. flags names the disagreements and the pairings that held: phone_country_differs_from_address_country, ip_country_differs_from_address_country, ip_country_differs_from_phone_country, disposable_email_from_datacenter_ip, disposable_email_from_anonymous_network, mail_drop_address_with_free_email_provider, disposable_phone_with_disposable_email, entity_country_differs_from_address_country, entity_name_differs_from_address_organization. The ip_address_distance comparison carries distance_km as evidence only, and never raises a flag: there is no honest distance at which a person stops being themselves.
The business identifier is compared against the postal address, the only other signal carrying a company name and a country of its own. entity_address_country puts the country of registration beside the country the address stands in; it is deliberately not part of country_agreement, because where a company is registered is not where a person is, and a Delaware company with a London office would otherwise read as a top-level disagreement. entity_address_organization compares the registered legal name with the organisation written on the address, and the only claim it makes is a normalised exact match: both sides are folded to their letters and digits and compared as strings, with case, accents (Société and Societe fold together), an ampersand against the word and, apostrophes, full stops in initials, and company-form words such as limited, ltd, plc, llp and inc taken out first — so a difference here is a difference in the name rather than in how it was typed. There is no similarity score, because no threshold honestly decides whether two names are one company — so differ means the two names are not the same name, not that the address belongs to somebody else. A value that folds away to nothing, such as an organisation line reading only "Ltd", withholds the comparison as a missing value.
There is no score, no risk level, no verdict and no recommendation, and there will not be one. The flags are the whole output, and a difference between two fields is a fact about the fields, not a judgement about a person — people travel, move house, keep a foreign number and browse through a VPN. Nothing about the call is stored beyond what each product already logs for its own lookup.
Request body
| field | type | description |
|---|---|---|
| string | null | The email address to verify. At most 254 characters. | |
| phone | string | null | The phone number to validate, in E.164 or in national notation with `country`. At most 32 characters. |
| country | string | null | The ISO region the phone number should be read against. It applies to the phone signal only; the postal address carries its own country. At most 2 characters. |
| ip | string | null | The IPv4 or IPv6 address the request came from. |
| address | object | null | The postal address, in the same shape `POST /api/v1/address` takes — either a written `address` or the separated fields, always with `country`. Send `organization` to have the company name compared with the register's. |
| entity | string | object | The business identifier, in the same shape `POST /api/v1/entity` takes. A bare string is read as a LEI, which is the only type needing no country. |
| entity.identifier | string | At most 64 characters. |
| entity.type | string | One of: lei, company_number. |
| entity.country | string | null | Required when `type` is `company_number`. At most 2 characters. |
Example request
curl -X POST https://spaw.co/api/v1/consistency \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"phone": "+442079460018",
"country": "GB",
"ip": "81.2.69.142",
"address": {
"address": "10 Downing Street, London, SW1A 2AA",
"organization": "ACME HOLDINGS LTD",
"country": "GB"
},
"entity": {
"identifier": "213800QILIUD4ROSUO03",
"type": "lei"
}
}'This endpoint has no console on its page. One call fans out to as many as four billed lookups, so a single click could spend four credits. It belongs in the dashboard, where the balance is on the screen beside it.
Responses
200Each signal's own answer, and the comparisons between them.
{
"success": true,
"data": {
"consistency": {
"signals_compared": [
"email",
"phone",
"ip",
"address",
"entity"
],
"country_agreement": "differ",
"flags": [
"ip_country_differs_from_address_country",
"ip_country_differs_from_phone_country"
],
"comparisons": [
{
"comparison": "phone_address_country",
"left": {
"signal": "phone",
"field": "country",
"value": "GB"
},
"right": {
"signal": "address",
"field": "country",
"value": "GB"
},
"result": "agree",
"withheld": null,
"flag": null,
"distance_km": null
},
{
"comparison": "ip_address_country",
"left": {
"signal": "ip",
"field": "country",
"value": "DE"
},
"right": {
"signal": "address",
"field": "country",
"value": "GB"
},
"result": "differ",
"withheld": null,
"flag": "ip_country_differs_from_address_country",
"distance_km": null
}
]
}
},
"meta": {
"requested": 5,
"processed": 5,
"credits_used": 5,
"credits_remaining": 995,
"stopped_reason": null,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}401The key is missing, malformed, or revoked.
{
"success": false,
"error": {
"code": "UNAUTHENTICATED",
"message": "Provide a valid API key as a bearer token.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}402The balance is empty. The lookup did not run.
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}422The request body could not be validated; `error.errors` lists the fields.
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}429Over 5 requests per second for the key. Retry after the limit resets.
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Retry after the limit resets.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}Error codes
Failures answer { success: false, error: { code, message, request_id } }. Each code has its own page.