How to validate phone numbers in Clay
Clay has no Boundstone app, and does not need one: an HTTP enrichment column and one endpoint gets you format, region and line type on every row — with an explicit record of what was not checked.
Contents
If you run outbound out of Clay, your phone numbers arrive from a dozen places — an Apollo export, a ZoomInfo enrichment, a scraped list, a form fill — and they arrive in a dozen shapes. Before those rows reach a dialer or an SMS send, it is worth knowing which ones are structurally real and what kind of line each one is.
There is no Boundstone app in Clay's catalog. There does not need to be one: Clay's HTTP API enrichment column calls any JSON endpoint, and that is the whole integration.
First, the honest part about Clay's own option
Clay ships a native mobile-validation action, and it is powered by Trestle. If you have not set up any phone validation at all, that is the path of least resistance and it works well.
So why add an HTTP column instead? One reason, and it is not "we are more accurate" — we have not published accuracy numbers, and we will not until benchmark № 001 does. The reason is disagreement. Line type and carrier are exactly the fields where vendors diverge, and on a list you are about to spend rep hours dialing, a second opinion on the rows that matter is cheap. What makes a disagreement useful rather than confusing is knowing what each side actually checked — which is the thing this API is built around.
Add the enrichment column
In your Clay table: Add enrichment → HTTP API.
| Field | Value |
|---|---|
| Method | POST |
| URL | https://api.boundstone.io/v1/verify/phone |
| Headers | Authorization: Bearer bs_live_YOUR_KEYContent-Type: application/json |
| Body | {"phone": "{{Phone}}", "country": "US"} |
Replace {{Phone}} with your table's phone column and bs_live_YOUR_KEY with a key from the dashboard. country is optional — it is the default region used when a number arrives without a + prefix, which in practice is most CRM exports.
A response looks like this — this is a real one, not a paraphrase:
{
"input": "+16504472983",
"valid": true,
"e164": "+16504472983",
"country": "US",
"line_type": "fixed_line_or_mobile",
"national_format": "(650) 447-2983",
"allocation": { "allocated": true, "reason": null, "snapshot": "2026-07-30" },
"checks": {
"performed": ["format", "region", "line_type_metadata", "allocation"],
"not_performed": ["carrier_lookup", "ported_status", "hlr_liveness"]
}
}
⚠️ country is worth setting. Without it, a number that arrives without a + prefix — which is most CRM exports — has no default region to parse against, and comes back valid: false with everything null. That is the API declining to guess, not a failure, but it will quietly gut a column if you skip it.
Map the fields you will actually sort on
Three are worth pulling into their own Clay columns:
e164— write this back over your original phone column. E.164 is the one format every dialer, CRM and SMS platform agrees on, and normalizing at enrichment time removes an entire class of downstream import failure. (national_formatis there too if you display numbers to reps.)line_type— the field you route on. See below.allocation.allocated— whether the number's block is actually assigned in the North American Numbering Plan atallocation.snapshot. This is how obviously fabricated numbers in a purchased list get caught: a well-formed number in an unassigned block is still well-formed, and still cannot ring.allocation.reasontells you which kind —unallocated_blockorreserved_fictional(the 555-01XX range). Outside the NANP,allocationisnullrather than implying coverage that does not exist.
Routing by line type
Clay conditional runs make this straightforward — filter the view, then act on each segment:
line_type |
What it means | Reasonable action |
|---|---|---|
mobile |
Cellular line | SMS and voice both available |
fixed_line |
Landline | Voice only — do not spend on SMS |
voip |
VoIP-designated range | Dial with lower expectation; common for spam traps and throwaways |
toll_free |
8XX | Usually a company main line, not a person |
fixed_line_or_mobile |
The plan cannot tell | See below |
fixed_line_or_mobile is not a bug or a gap we are working around. In much of the North American plan, mobile and landline numbers are drawn from the same ranges, so the metadata genuinely cannot separate them — and a vendor that returns a confident mobile for those rows is guessing. If that split matters to your send, it needs the live network, not better metadata.
When you need to know a number is live
Everything above is metadata. It tells you a number is well-formed, in an allocated block, and what kind of line it is. It does not tell you the number is assigned to someone, still in service, or that a call will connect.
For that, add "hlr": true to the request body. That runs a live HLR lookup against the carrier network and returns carrier, ported status and reachability. It costs 5 credits instead of 1, it is never run on the free tier, and when the network declines to answer, the row is refunded rather than guessed at.
Use it as a second pass on the segment you care about — the rows that survived the cheap filters — rather than across the whole table.
What this does not do
Worth stating plainly, because a Clay column that quietly implies more than it checked is worse than no column:
- It is not DNC or TCPA scrubbing. Validation removes numbers that cannot be real; it says nothing about whether you are permitted to call one. That needs a registry vendor — see DNC vs phone validation.
- It is not identity matching. Nothing here confirms a number belongs to the person on the row.
- Without
hlr:true, it is not liveness — and not carrier either. Every response carriesnot_performed: ["carrier_lookup", "ported_status", "hlr_liveness"], which is the point: you can read what was skipped instead of inferring it.
The short version
Clay needs no Boundstone app: an HTTP enrichment column pointed at /v1/verify/phone gives you E.164 normalization, allocation-aware validity and line type on every row, for one credit each. Set country so unprefixed numbers parse. Route SMS on mobile, keep fixed_line on voice, treat fixed_line_or_mobile as genuinely unknown, and spend the paid HLR dip only on the segment that earns it. Every response tells you which checks ran and which did not — so what you push back into your table is a fact, not an impression.