JSON over HTTPS. Every endpoint is also available as a tool on the MCP server.
Basics
Base URL: https://api.wholayer.com/v1
Authenticate with a secret key from Settings, API keys, sent as a Bearer token. Keys are scoped to one workspace. Never put them in browser code.
curl https://api.wholayer.com/v1/people/enrich \
-H "Authorization: Bearer wl_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4f1d9c2e-order-123" \
-d '{"linkedin_url":"https://www.linkedin.com/in/dana-okafor","reveal":["work_email"]}'Send an Idempotency-Key header on POST requests. Retrying with the same key returns the original result and never charges twice. Every response includes credits_used and an X-Credits-Remaining header.
One call can spend at most 250 credits. To allow more, send X-Max-Credits: 1000 (up to 10,000). Test keys (wl_test_...) return sandbox data on every endpoint, never spend credits, and add "test_mode": true to responses.
Endpoints
POST /people/enrich
Find a person's work email, personal email, mobile, and profile from any identifier you have.
Request
{
"linkedin_url": "https://www.linkedin.com/in/dana-okafor",
"email": null,
"name": "Dana Okafor",
"company_domain": "fieldline.com",
"reveal": ["work_email", "personal_email", "mobile"]
}Response
{
"id": "per_8f2c1a",
"name": "Dana Okafor",
"title": "VP Revenue",
"seniority": "vp",
"department": "sales",
"linkedin_url": "https://www.linkedin.com/in/dana-okafor",
"company": { "name": "Fieldline", "domain": "fieldline.com", "employees": 210 },
"work_email": { "address": "dana@fieldline.com", "risk": "A", "verified_at": "2026-11-01T18:22:04Z" },
"personal_email": { "address": "dana.okafor@gmail.com", "risk": "B", "verified_at": "2026-10-28T09:10:51Z" },
"mobile": { "number": "+15125550134", "line_type": "mobile", "carrier": "T-Mobile", "checked_at": "2026-11-02T14:03:11Z" },
"credits_used": 7
}POST /people/search
Search people by title, seniority, department, company size, industry, and location. Returns profiles without contact details.
Request
{
"titles": ["head of revops", "vp revenue"],
"company_employees": { "min": 50, "max": 500 },
"industries": ["software"],
"countries": ["US"],
"limit": 25,
"cursor": null
}Response
{
"results": [
{ "id": "per_8f2c1a", "name": "Dana Okafor", "title": "VP Revenue", "company_domain": "fieldline.com" }
],
"next_cursor": "c_01HXZ...",
"credits_used": 1
}POST /emails/verify
Check whether up to 1,000 email addresses will deliver, with a risk grade from A (safe) to F (will bounce).
Request
{ "emails": ["dana@fieldline.com", "old.address@example.com"] }Response
{
"results": [
{ "email": "dana@fieldline.com", "status": "deliverable", "risk": "A", "catch_all": false },
{ "email": "old.address@example.com", "status": "undeliverable", "risk": "F", "reason": "mailbox_not_found" }
],
"credits_used": 0.2
}POST /phones/lookup
Find a mobile number for a person, with a live carrier and line-type check.
Request
{ "person_id": "per_8f2c1a" }Response
{
"number": "+15125550134",
"line_type": "mobile",
"carrier": "T-Mobile",
"checked_at": "2026-11-02T14:03:11Z",
"credits_used": 5
}GET /companies/{domain}
Company profile: size, revenue range, industry, location, and detected technologies.
Response
{
"domain": "fieldline.com",
"name": "Fieldline",
"employees": 210,
"revenue_range": "$10M-$50M",
"industry": "Software",
"hq": { "city": "Austin", "region": "TX", "country": "US" },
"technologies": ["HubSpot", "Segment", "Stripe"],
"credits_used": 0.2
}GET /balance
Credits remaining, plan, monthly spend cap, and this key's daily cap.
Response
{
"credits_remaining": 4210.5,
"plan": "growth",
"monthly_spend_cap": 500,
"key_daily_cap": 1000,
"key_mode": "live"
}POST /bulk
Enrich or verify up to 1,000 rows in one synchronous request. Each row is billed like a single call. Stops early if your wallet runs out.
Request
{
"operation": "enrich",
"rows": [
{ "name": "Dana Okafor", "company_domain": "fieldline.com", "reveal": ["work_email"] },
{ "linkedin_url": "https://www.linkedin.com/in/priya-n", "reveal": ["work_email", "mobile"] }
]
}Response
{
"results": [
{ "ok": true, "id": "per_8f2c1a", "work_email": { "address": "dana@fieldline.com", "risk": "A" }, "credits_used": 1 },
{ "ok": false, "error": { "code": "person_not_available", "message": "No match for this person. You were not charged." } }
],
"credits_used": 1
}GET /visitors?site_id=site_123&since=2026-11-01T00:00:00Z&level=person
List people and companies identified on your sites. Paginated with cursor.
Response
{
"results": [
{
"id": "vis_77a1",
"level": "person",
"person_id": "per_8f2c1a",
"company_domain": "fieldline.com",
"first_seen": "2026-11-02T15:41:09Z",
"pages": [{ "url": "/pricing" }, { "url": "/compare" }],
"visits_this_month": 3
}
],
"next_cursor": null
}Suppressed and opted-out people
If a person has opted out or is on your own suppression list, every endpoint behaves as if no match exists: you get 404 person_not_available and are not charged. We don't distinguish opted-out people from unknown people in responses, so don't build logic that depends on it.
Webhooks
Events are POSTed as JSON to your endpoint. Every request carries a WhoLayer-Signature header in the form t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of t + "." + rawBody using your workspace webhook secret (Destinations page). Recompute it, compare in constant time, and reject timestamps more than 5 minutes old to block replays. Respond with any 2xx within 10 seconds; failed deliveries are retried hourly for up to 24 hours. Company-level matches use the same visitor.identified event with data.level set to "company" and data.person set to null.
// Node.js verification
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}visitor.identified
{
"type": "visitor.identified",
"id": "evt_19ab",
"created": "2026-11-02T15:41:12Z",
"site_id": "site_123",
"data": {
"identity_id": "8f2c1a7e-…",
"level": "person",
"person": { "id": "8f2c1a7e-…", "name": "Dana Okafor", "title": "VP Revenue",
"linkedin_url": "https://www.linkedin.com/in/dana-okafor",
"work_email": "dana@fieldline.com", "location": "Austin, TX" },
"company": { "name": "Fieldline", "domain": "fieldline.com", "employees": 210 },
"page": { "url": "/pricing", "referrer": null },
"pages": [{ "url": "/compare" }, { "url": "/pricing" }],
"visits_this_month": 3,
"first_seen": "2026-11-01T18:20:04Z",
"last_seen": "2026-11-02T15:41:12Z",
"source": "wholayer"
}
}shopper.identified
{
"type": "shopper.identified",
"id": "evt_19ac",
"created": "2026-11-02T16:02:40Z",
"site_id": "site_456",
"data": {
"identity_id": "c41d09b2-…",
"email": "marco.reyes@example.com",
"email_risk": "A",
"first_name": "Marco",
"pages": [{ "url": "/products/linen-shirt" }, { "url": "/cart" }],
"visits_this_month": 2,
"source": "wholayer"
}
}Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A field is missing or malformed. The message says which. |
| 401 | unauthorized | Missing or invalid API key. |
| 402 | credits_exhausted | Your wallet is empty or you've hit your spend cap. |
| 403 | forbidden | Read-only key used on an endpoint that reveals contact details. |
| 404 | person_not_available | No match, or the person has opted out. You are not charged. |
| 409 | duplicate_request | Same Idempotency-Key reused with a different body. |
| 409 | request_in_progress | A request with this Idempotency-Key is still running. Retry after a few seconds to get its stored result. |
| 413 | payload_too_large | More than 1,000 rows sent to /bulk, or a body over 4 MB. |
| 429 | rate_limited | Too many requests. Retry after the Retry-After header. |
| 500 | internal_error | Our fault. Retry with the same Idempotency-Key. |
| 503 | provider_unavailable | A data source is temporarily unavailable. You are not charged. Retry later. |
Error bodies look like {"error":{"code":"rate_limited","message":"..."}}.
Rate limits
| Plan | Requests per second |
|---|---|
| Free | 2 |
| Starter | 10 |
| Growth | 25 |
| Scale | 100 |
| Platform | Custom, up to 1,000 |
For more than 1,000 records, send several POST /bulk requests in sequence. Ask us about file and warehouse delivery on the Platform plan.