Live: Tor + abuse feeds refreshed every 6 hours

LayerCall

Postman

Tooling

What exists today

Nothing to build — this is the fastest way to see a real response before writing any code. Worth doing properly once, because a saved collection with tests doubles as the integration test you would otherwise never write.

Setup

  1. 1Create a collection and add a collection variable named apiKey, type Secret, so it is not synced to your team's shared workspace.
  2. 2Add a collection-level header X-Api-Key with value {{apiKey}} so every request inherits it.
  3. 3Add a POST request to https://www.layercall.com/v1/score/user with a raw JSON body.
  4. 4Paste the test script below into the Tests tab.
  5. 5Run the collection with the Collection Runner, or in CI with Newman.

Code

// Postman → Tests tab
//
// These are the assertions worth keeping. They check the contract, not the
// score — a score changes as intelligence improves, but a missing field
// breaks your code.

const data = pm.response.json();

pm.test("200 OK", () => pm.response.to.have.status(200));

pm.test("responds inside 2s", () =>
  pm.expect(pm.response.responseTime).to.be.below(2000));

pm.test("contract fields present", () => {
  pm.expect(data).to.have.property("risk_score").that.is.a("number");
  pm.expect(data).to.have.property("verdict").that.is.a("string");
  pm.expect(data).to.have.property("summary").that.is.a("string");
  pm.expect(data).to.have.property("components_checked").that.is.an("array");
});

pm.test("verdict is one of the three", () =>
  pm.expect(["allow", "review", "block"]).to.include(data.verdict));

// Every component you asked for should come back, or be reported as
// timed out — silence would mean a partial answer you cannot detect.
pm.test("no silent partial", () => {
  const asked = ["ip", "email", "phone"];
  const seen = data.components_checked.concat(data.components_timed_out ?? []);
  asked.forEach((c) => pm.expect(seen).to.include(c));
});

pm.collectionVariables.set("lastRequestId", data.request_id);

What to do with each verdict

Three outcomes, and the middle one is the one worth getting right — refusing a real customer usually costs more than reviewing them.

VerdictIn Postman
allowNothing to do — this is the case you want your smoke test to hit, using a known-good input.
reviewUseful as a fixture. Point one saved request at a Tor exit node so the collection always exercises a non-trivial path.
blockWorth a second saved request against a disposable-email address, so a change that stops catching disposables fails your collection rather than your customers.

What comes back

A real response, generated from the live API rather than written by hand. Branch on verdict; summary is a sentence written to be shown to a person, and components_checked tells you what actually went into the score.

Show the full response(POST /v1/score/user)
{
  "risk_score": 65,
  "verdict": "review",
  "summary": "Needs review (65/100) — Tor exit node, commercial VPN and datacenter ASN.",
  "components": {
    "email": {
      "email": "test@guerrillamail.com",
      "normalized_email": "test@guerrillamail.com",
      "risk_score": 100,
      "verdict": "block",
      "status": "do_not_mail",
      "sub_status": "disposable",
      "deliverability_score": 0,
      "did_you_mean": null,
      "signals": {
        "syntax_valid": true,
        "mx_found": true,
        "is_disposable": true,
        "is_homograph": false,
        "is_role_account": true,
        "is_free_provider": false,
        "is_suspicious_handle": true,
        "is_tagged": false,
        "is_risky_tld": false,
        "is_new_domain": null,
        "has_spf": true,
        "has_dmarc": true,
        "has_website": true,
        "mailbox_exists": null,
        "is_catch_all": null,
        "mailbox_status": "unavailable",
        "has_digital_footprint": null
      },
      "domain": "guerrillamail.com",
      "domain_age_days": null,
      "mx_provider": null,
      "mx_records": [
        "mail.guerrillamail.com."
      ],
      "abuse_reports": 0,
      "digital_footprint": {
        "has_gravatar": false,
        "gravatar_profile_url": null,
        "breach_count": null,
        "seen_in_breach": null
      }
    },
    "phone": {
      "parse_status": "ok",
      "phone": "+14155552671",
      "risk_score": 0,
      "signals": {
        "syntax_valid": true,
        "is_possible": true,
        "is_voip": false,
        "is_premium_rate": false,
        "is_toll_free": false,
        "assigned_area_code": true,
        "is_fictional": false
      },
      "number": {
        "e164": "+14155552671",
        "country": "US",
        "national": "(415) 555-2671",
        "international": "+1 415 555 2671",
        "line_type": "fixed_line_or_mobile"
      },
      "verdict": "allow",
      "abuse_reports": 0
    },
    "ip": {
      "ip": "185.220.101.1",
      "risk_score": 60,
      "signals": {
        "is_vpn": true,
        "is_proxy": true,
        "is_datacenter": true,
        "is_tor": true,
        "recent_abuse": false,
        "is_hijacked_netblock": false
      },
      "geo": {
        "country": "DE",
        "city": "Berlin",
        "asn": "AS60729",
        "isp": "Stiftung Erneuerbare Freiheit"
      },
      "vpn_provider": null,
      "hijacked_source": null,
      "verdict": "review",
      "abuse_reports": 0
    }
  },
  "components_checked": [
    "email",
    "phone",
    "ip"
  ],
  "linkage": {
    "device_email_count": null,
    "email_device_count": 0,
    "email_ip_count": 0,
    "subnet_rate_1h": 2,
    "domain_rate_1h": 0
  },
  "actor": {
    "type": "unknown",
    "proven": false,
    "basis": "none",
    "operator": null,
    "trigger": null,
    "detail": "No signature and no device fingerprint. Drop fp.js on the page, or pass the agent's signed request, to get an answer here."
  },
  "top_signals": [
    "ip: tor exit node",
    "ip: commercial vpn",
    "ip: datacenter asn",
    "email: disposable domain",
    "email: role account",
    "email: machine-generated handle"
  ]
}

Traps specific to Postman

A plain collection variable syncs your live key to the team workspace

Set the variable type to Secret. An ordinary variable is stored in the collection and shared with everyone it is shared with, including anyone added later.

Asserting on risk_score makes the test flaky by design

The score moves as intelligence improves — that is the product working. Assert on the shape and on verdict membership, not on the number, or your suite goes red every time we get better at something.

Newman needs the key passed in, not committed

Export the collection without the environment, and supply the key in CI with --env-var apiKey=$LAYERCALL_KEY. A collection JSON with a live key inside it in your repo is a leaked key.

Questions

Is there a published Postman collection?

Not in the public API Network. Build it from the endpoint reference in the docs — it takes a few minutes and you end up with something matching the endpoints you actually use rather than all of them.

Can I run this in CI?

Yes, with Newman. Use a tl_test_ key so a CI run on every commit does not bill lookups — test mode returns fixtures with the same response shape, which is exactly what these assertions check.