Bubble
Bubble's API Connector handles this with no plugin. The part that trips everyone up is initialisation: Bubble learns the response shape from one sample call, and any field missing from that sample does not exist for the rest of your app.
Setup
- 1Install the API Connector plugin (it ships with Bubble) and add a new API named LayerCall.
- 2Add a shared header X-Api-Key with your key, set to Private so it stays server-side.
- 3Add a call: POST to https://www.layercall.com/v1/score/user, body type JSON, using the body below.
- 4Mark ip, email and phone as parameters, untick Private on those so workflows can set them.
- 5Initialise the call with values that return a full response — see the pitfall — then use it as an action in a workflow.
Code
{ "ip": "<ip>", "email": "<email>", "phone": "<phone>" }
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.
| Verdict | In Bubble |
|---|---|
| allow | Continue the workflow — create the user, sign them up, whatever came next. |
| review | Set a risk field on the User thing and continue. Use a Only when condition on later actions rather than a separate branch, which Bubble makes awkward. |
| block | Add a Terminate this workflow action with a Only when condition of Result of step 1's verdict is block, and show an alert carrying the summary. |
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 Bubble
Bubble builds the data type from the initialisation response. Initialise with only an email and the ip fields never appear in the expression composer — and re-initialising later can break every expression already built on the old shape. Initialise once, with ip, email AND phone all populated.
Private means Bubble fills the value server-side and never exposes it. That is right for the API key and wrong for ip/email/phone, which your workflow needs to set. Getting it backwards either leaks the key or leaves you unable to pass data.
There is no built-in 'current user's IP' in Bubble. You need a plugin that exposes it, or you pass it from a page-load workflow. Sending an empty ip is not an error — it just scores as unknown, so the call looks like it worked while measuring nothing.
Questions
Does this need a paid Bubble plan?
The API Connector works on all plans including free. Server-side workflows and scheduled workflows have plan limits, but a single API call from a page workflow does not.
Action or data source?
Action. Set it to 'Use as Action' so it runs once when the workflow reaches it. As a data source Bubble may call it repeatedly while re-rendering, and each call is a billed lookup.