Retool
Retool talks to any REST API through a resource. Nothing LayerCall-specific is needed, and the API key lives in the resource rather than in a component.
Setup
- 1Create a REST API resource with base URL https://www.layercall.com
- 2Add a default header X-Api-Key with your key so it is never in front-end code.
- 3Add a query POSTing to /v1/score/user with the fields from your table row.
- 4Bind the verdict to a Tag component so reviewers see allow / review / block at a glance.
Code
// Retool → Transformer, bound to a Tag component's Value. // Turns the verdict into Retool's own colour vocabulary so a reviewer // reads the queue without reading numbers. const r = {{ scoreUser.data }}; if (!r || {{ scoreUser.error }}) { return { label: "unscored", colour: "neutral" }; } return { allow: { label: "Allow", colour: "success" }, review: { label: "Review", colour: "warning" }, block: { label: "Block", colour: "danger" }, }[r.verdict] ?? { label: r.verdict, colour: "neutral" };
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 Retool |
|---|---|
| allow | Leave the row actionable — the reviewer has nothing to do. |
| review | Surface r.summary in a Callout above the row's action buttons, so the reviewer sees why before deciding. |
| block | Disable the approve button with a disabled expression on the same transformer, rather than hiding the row. A hidden row looks like a bug to whoever is on shift. |
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 Retool
Retool resources run server-side, so headers configured there are never sent to the client. Headers added inside a query's component-level config are a different thing and are visible in devtools. Set it once, on the resource.
A scoring query with 'Run on page load' enabled fires for every row the table renders, which bills a lookup per row per refresh. Switch it to manual and trigger it from the row selection instead.
Questions
Will the key be exposed to users of the app?
Not if it is set on the resource. Retool resources run server-side, so headers configured there are never shipped to the browser. Putting the key in a component's headers would expose it — do not.
Can I score a whole table at once?
Yes, with /v1/batch — one query, up to 500 values of one type. That is far cheaper than a per-row query firing on load, which is the usual way a Retool bill gets surprising.