Skip to content

Evidence about the details, not a verdict about a person.

One POST request takes any subset of an email address, a phone number, an IP address and a postal address, runs each through the product that owns it, and names where the answers agree and where they do not — a country roll-up, eight comparisons that quote both sides, and nine named flags.

There is no score, no risk level, no verdict and no recommendation, and there will not be one. That refusal is the product.

Free credits every month. No credit card required. Each signal bills as its own lookup; the comparison is free.

$ curl 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", "country": "GB"}}'

{
  "success": true,
  "data": {
    "signals": { "email": { … }, "phone": { … }, "ip": { … }, "address": { … } },
    "consistency": {
      "signals_compared": ["email", "phone", "ip", "address"],
      "country_agreement": "differ",
      "flags": ["ip_country_differs_from_address_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
        },
        {
          "comparison": "ip_address_country",
          "left":  { "signal": "ip", "field": "country", "value": "DE" },
          "right": { "signal": "address", "field": "country", "value": "GB" },
          "result": "differ", "flag": "ip_country_differs_from_address_country"
        },
        …
      ]
    }
  },
  "meta": { "requested": 4, "processed": 4, "credits_used": 4, "credits_remaining": 996 }
}
five signals in one calleight comparisons, both sides quotednine named flagsno score, no verdict, no recommendationthe comparison itself is free

What this endpoint will not do.

This is the one endpoint whose output is an inference about a person, so the rules it refuses to break matter more than anything it adds. All three are enforced in the code and pinned by tests.

There is no score, and there will not be one

No score, no risk level, no verdict, no recommendation. The flags are the whole output. 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 after emigrating, and browse through a VPN because they would rather not be tracked. Rolling that into one number would hide which fact drove it, and a number that decides about a person is a number you have to be able to explain to that person.

A null withholds; it never disagrees

Where either side of a comparison is null, the result is not_compared with withheld: missing_value. Across every Spaw product a null means “not evaluated” and never “no” — an invalid phone number carries no country, a reserved IP carries no location, an address outside every installed mail-drop list carries no mail-drop answer. Comparing against a null would turn a gap in the data into a finding about a person.

A VPN is not a lie

Where the IP is a VPN exit, a relay or a datacenter egress, every country comparison involving it is withheld as low_confidence — that country belongs to the network operator, not to the person behind it, and reporting a difference there would flag everyone who uses a VPN. If the network itself is what you want to act on, is_anonymous, is_relay and is_datacenter are right there on the IP block.

One request, and every signal’s own full answer.

data.signals carries one entry per product and all five keys are always present. Each block is exactly what that product’s own endpoint returns, field for field — the 27 email fields, the 33 phone fields, the 46 IP fields and the 45 address fields, each with its own sources — so nothing has to be learned twice and nothing is summarised away.

  • Send any one of email, phone, ip and address, or any combination
  • A key is null when you did not ask for that signal, or when the call did not reach it
  • The legs run ip, phone, address, email — a keyed body has no input order
  • No new dataset, no network call and no third party: the analysis reads answers already computed
  • Running out of credits part-way keeps what was paid for and reports stopped_reason
consistency What it tells you
signals_comparedstring[]Which of email, phone, ip and address answered and were available to compare.
country_agreementstring | nullThe roll-up over the three country comparisons only: differ if any of them differed, agree if at least one agreed and none differed, and null when none of them could be made — never a silent agree.
flagsstring[]Every flag raised, in comparison order. The seven below, each with its own reference page.
comparisonsobject[]Every comparison that was possible, including the ones withheld, each with both sides quoted.

Eight checks, and the fields that decided each one.

Three countries against each other, one distance carried as evidence, and the pairings where two weak signals coinciding says more than either alone. A comparison is emitted only when both of its signals answered — a signal nobody asked about produces silence, not a row.

Comparison What it says
phone_address_countryThe country the number was allocated under, against the country the address stands in. Both come from registers; neither is a guess.
ip_address_countryWhere the request came from, against where the post would go. Withheld when the IP is a VPN exit, a relay or a datacenter egress.
ip_phone_countryThe same comparison against the numbering plan, withheld under the same conditions.
ip_address_distanceHow far apart the two places are, in kilometres, as evidence only. Measured just once the two countries already agree and the IP location is graded high confidence. It never raises a flag.
email_ip_disposable_datacenterA burner address arriving from hosting rather than from a consumer network.
email_ip_disposable_anonymousA burner address arriving over a VPN, a proxy or Tor.
email_address_free_provider_mail_dropA free consumer mailbox beside a commercial mail-receiving agency.
email_phone_disposableBoth contact details on disposable lists: neither can receive anything for long.

Every row quotes both sides.

This is the shape of one entry in comparisons. Nothing is asserted that you cannot see the working for, and a comparison that was not made says why it was not made.

Field What it tells you
comparisonWhich of the eight checks this row is.
left / rightBoth sides, each naming the signal, the field on that signal’s own block, and the value it held. This is the working: nothing is asserted you cannot read for yourself.
resultagree when the values matched or the pairing held, differ when two known values disagreed, not_compared when nothing was settled. A pairing holds only when both sides are true — two known-false fields are not a disagreement, they are simply not the thing being looked for.
withheldWhy nothing was settled: missing_value when a side was null, low_confidence when the IP speaks for its operator rather than for a person. Null when the comparison was actually made.
flagThe flag this comparison raised, if any. Flags live in flag rather than reason, because they describe two signals rather than one.
distance_kmFilled only on ip_address_distance, as evidence. No threshold is applied and no flag is ever raised from it: there is no honest distance at which a person stops being themselves.

Seven flags, and nothing above them.

The flags are the whole output. Each names one thing that is true about two fields you sent, each has its own reference page, and none of them is summed, weighted or rolled into a verdict.

Flag What it says
phone_country_differs_from_address_countryThe numbering plan the phone belongs to is not the country the address stands in.
ip_country_differs_from_address_countryThe request came from one country and the post would go to another — raised only where the IP genuinely speaks for a place.
ip_country_differs_from_phone_countryThe same difference against the numbering plan.
disposable_email_from_datacenter_ipA disposable mailbox arriving from a datacenter range rather than a consumer network.
disposable_email_from_anonymous_networkA disposable mailbox arriving over a VPN, a proxy or Tor.
mail_drop_address_with_free_email_providerA free consumer mailbox given alongside an address a mail-receiving agency operates.
disposable_phone_with_disposable_emailBoth the mailbox and the number are on disposable lists: neither can receive anything for long.

Signup, onboarding, marketplace vetting.

Vet a signup with what you already collect

At the moment before there is an order, a card or a payment, the contact details are all you have. A phone allocated in one country beside an address in another, a burner mailbox arriving from a datacenter, a mail-drop address beside a free consumer provider: none of that is visible to a single lookup, and all of it is visible here.

Write a rule you can defend

The output is named flags and quoted values, so a review queue can show the reviewer exactly which two fields disagreed and what each of them said. A rule written from those will fit your own customers better than a generic score, and you will be able to explain it to the person it was applied to.

One call instead of five

Each signal returns its own full answer, field for field, so the consistency call replaces the separate lookups rather than adding to them. Each leg bills exactly as it would on its own endpoint, and the analysis on top is free.

Common questions about consistency checks.

Do I get a score or a risk level?

No. There is no score, no risk level, no verdict and no recommendation, and there will not be one. What you get is country_agreement over the three country comparisons, the list of comparisons with both sides quoted, and the flags that were raised. A difference between two fields is a fact about the fields, not a judgement about a person — so the endpoint hands you the evidence and stops, and you weigh it against what you know about your own customers.

Which signals can I send?

Five: email, phone, ip, address and entity. Any one of them, or any combination — a body with none of the five is a validation error. Each field takes what its own single-lookup endpoint takes, so address is an object with either a written address or the separated fields, always with its own country, while the top-level country is the region hint for the phone number only. Every comparison is arithmetic over fields those five pipelines already computed: nothing here runs a check the single endpoints do not.

What happens when I only send two signals?

You get the comparisons those two make possible, and silence for the rest. A comparison is emitted only when both of its signals answered, so a signal nobody asked about produces no row at all rather than a row saying nothing could be compared. The signals block still carries all five keys, with null for the ones you did not ask for.

Why is the country comparison missing for my VPN user?

Because it was withheld on purpose. When the IP is a VPN exit, a relay or a datacenter egress, the country belongs to the network operator rather than to the person, so every country comparison involving that IP answers not_compared with withheld: low_confidence. Flagging it would flag everyone who uses a VPN, which is a great many ordinary people. The IP block still carries is_anonymous, is_relay and is_datacenter if the network itself is what you want to act on.

What is distance_km for, if it never raises a flag?

Evidence, and nothing more. It is measured only once the IP and address countries already agree and the IP location is graded high confidence, and it is reported so you can see how far apart the two places were. No threshold is applied, because there is no honest distance at which a person stops being themselves — plenty of people order to an address they are not currently sitting at.

What does it cost?

Each signal bills exactly as its own single-lookup endpoint: 1 credit for a definitive answer, free when the answer is no, and free for a seven-day repeat of the same input on the same account. meta.credits_used is the sum of the per-signal costs, and the consistency analysis itself is always free — it reads answers you already paid for and adds no dataset, no network call and no third party. A call that runs out of credits part-way keeps the signals it paid for, answers 200 and reports meta.stopped_reason; only a call refused on its very first signal answers 402.

Is anything about the call stored?

Nothing beyond what each product already logs for its own lookup. The comparisons are computed and discarded with the response. Suppression lists are not applied here either: a consistency check is a fresh vetting decision, and a fresh lookup is how an address earns its way off a list. The endpoint keeps its own rate-limit bucket, because one call fans out to as many as five lookups.

Compare your first five signals.

Create a key, send one POST request, and keep free lookups every month on us.

Get your API key