The GeoLayer API is organized around REST. Our API has predictable resource-oriented URLs, accepts JSON-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes.
The GeoLayer API uses API keys to authenticate requests. You can view and manage your API keys in the GeoLayer Dashboard.
Rate limits are applied based on your subscription tier. If you exceed the rate limit, the API will return a 429 Too Many Requests error. API access requires a paid plan (Starter or higher) — Free plan users must use the dashboard.
| Plan | Rate Limit | Included Leads / mo |
|---|---|---|
| Starter | 5 req/sec | 98 |
| Growth | 10 req/sec | 220 |
| Pro | 50 req/sec | 1,500 |
| Enterprise | 200 req/sec | 5,000 |
Top up any time at $0.50 / lead ($50 minimum) — credits never expire.
GeoLayer uses conventional HTTP response codes to indicate the success or failure of an API request.
Receive real-time push notifications when your search tasks complete — faster and more reliable than polling. Set your webhook URL via the dashboard or PUT /v1/webhooks. Every delivery is signed with HMAC-SHA256 and retried on transient failure.
| Event | When it fires |
|---|---|
| task.completed | Search task finished successfully with at least 1 lead returned. |
| task.out_of_credits | Task short-circuited because your account balance hit zero mid-fetch. |
{
"timestamp": "2026-05-26T18:42:11.000Z",
"event": "task.completed",
"submission_id": "job_8291x_q",
"total_leads": 10,
"credits_used": 10,
"credits_remaining": 188,
"task": { "keyword": "Plumber", "city": "Austin", "max_leads": 10 }
}Every request carries an X-Geolayer-Signature: sha256=<hex> header. Compute the same HMAC-SHA256 of the raw request body using your shared secret (set as OUTBOUND_WEBHOOK_SECRET; falls back to your JWT secret if unset). Compare with timing-safe equality — never ===.
app.post("/webhook", express.raw({type: "*/*"}), (req, res) => {
const sig = req.headers["x-geolayer-signature"];
const hmac = crypto.createHmac("sha256", process.env.SECRET);
const expected = "sha256=" + hmac.update(req.body).digest("hex");
const a = Buffer.from(sig);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString());
// handle event.submission_id ...
res.status(200).end();
});import hmac, hashlib, os
from flask import request
@app.post("/webhook")
def webhook():
sig = request.headers.get("X-Geolayer-Signature", "")
expected = "sha256=" + hmac.new(
os.environ["SECRET"].encode(),
request.get_data(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(sig, expected):
return "", 401
event = request.get_json()
# handle event["submission_id"] ...
return "", 200require "openssl"
require "json"
post "/webhook" do
raw = request.body.read
expected = "sha256=" + OpenSSL::HMAC.hexdigest(
"SHA256", ENV["SECRET"], raw
)
sig = request.env["HTTP_X_GEOLAYER_SIGNATURE"].to_s
halt 401 unless Rack::Utils.secure_compare(sig, expected)
event = JSON.parse(raw)
# handle event["submission_id"] ...
200
end$raw = file_get_contents("php://input");
$sig = $_SERVER["HTTP_X_GEOLAYER_SIGNATURE"] ?? "";
$expected = "sha256=" . hash_hmac(
"sha256", $raw, getenv("SECRET")
);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
// handle $event["submission_id"] ...
http_response_code(200);submission_id. Use it as your dedup key.Send a signed test payload to your configured URL without running a real task:
curl -X POST https://geolayer.io/v1/webhooks/test \ -H "Authorization: Bearer YOUR_API_KEY"
No dedicated SDK package — the API is small enough that a 4-line HTTP call is cleaner than a wrapper. Here's idiomatic code in the languages we get asked about most. Same endpoints, same payloads, swap your YOUR_API_KEY.
const KEY = process.env.GEOLAYER_API_KEY;
const BASE = "https://geolayer.io";
async function pullLeads(keyword, city, max = 50) {
// 1. submit
const submit = await fetch(`${BASE}/v1/tasks/submit`, {
method: "POST",
headers: {
"Authorization": `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ keyword, city, max_leads: max }),
}).then(r => r.json());
// 2. poll until done (or use a webhook)
while (true) {
await new Promise(r => setTimeout(r, 3000));
const res = await fetch(
`${BASE}/v1/tasks/results/${submit.submission_id}`,
{ headers: { "Authorization": `Bearer ${KEY}` } }
).then(r => r.json());
if (res.status === "completed") return res.data;
if (res.status === "error" || res.status === "out_of_credits") {
throw new Error(res.reason || res.status);
}
}
}
pullLeads("Plumber", "Austin", 20).then(console.log);import os, time, requests
KEY = os.environ["GEOLAYER_API_KEY"]
BASE = "https://geolayer.io"
H = {"Authorization": f"Bearer {KEY}"}
def pull_leads(keyword, city, max_leads=50):
# 1. submit
r = requests.post(f"{BASE}/v1/tasks/submit",
headers={**H, "Content-Type": "application/json"},
json={"keyword": keyword, "city": city, "max_leads": max_leads})
sid = r.json()["submission_id"]
# 2. poll
while True:
time.sleep(3)
res = requests.get(
f"{BASE}/v1/tasks/results/{sid}", headers=H
).json()
if res["status"] == "completed":
return res["data"]
if res["status"] in ("error", "out_of_credits"):
raise RuntimeError(res.get("reason") or res["status"])
print(pull_leads("Plumber", "Austin", 20))require "net/http"
require "json"
require "uri"
KEY = ENV["GEOLAYER_API_KEY"]
BASE = URI("https://geolayer.io")
def pull_leads(keyword, city, max_leads = 50)
body = JSON.generate(
keyword: keyword, city: city, max_leads: max_leads
)
h = {
"Authorization" => "Bearer #{KEY}",
"Content-Type" => "application/json",
}
sid = JSON.parse(Net::HTTP.post(
URI("#{BASE}/v1/tasks/submit"), body, h
).body)["submission_id"]
loop do
sleep 3
res = JSON.parse(Net::HTTP.get(
URI("#{BASE}/v1/tasks/results/#{sid}"),
"Authorization" => "Bearer #{KEY}"
))
return res["data"] if res["status"] == "completed"
raise(res["reason"] || res["status"]) if %w[error out_of_credits].include?(res["status"])
end
end
puts pull_leads("Plumber", "Austin", 20)# Sync single-business enrichment (no polling).
curl -X POST https://geolayer.io/v1/get-profile \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Plumbing",
"city": "Austin",
"state": "TX"
}'
# Bulk CSV (returns text/csv directly).
curl -G https://geolayer.io/v1/leads/export \
-H "Authorization: Bearer YOUR_API_KEY" \
-d keyword=Plumber -d city=Austin -d limit=500 \
-o austin-plumbers.csvA working Postman collection is published as a public workspace. Import directly:
Downloadpostman-collection.json
Includes every endpoint with example payloads, environment variables for {{API_KEY}}, and tests that assert response shape. Import → set your API key → ready.
Anywhere your tool supports an HTTP / webhook call, you can call GeoLayer. Below are the integration patterns we get asked about most often. None of these require a special connector — the standard REST API handles them.
Add an HTTP-enrichment column that calls /v1/get-profile per row. Map name + city from existing columns.
// In Clay → Add Column → HTTP API → POST
URL: https://geolayer.io/v1/get-profile
Method: POST
Headers: Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Body: {
"name": {{Company Name}},
"city": {{City}},
"state": {{State}}
}
// Output columns auto-flatten: response.email, response.phone, etc.Either (a) bulk-export from GeoLayer and upload the CSV into a Smartlead campaign, or (b) use a Zapier zap: New Task Completed → Add Leads to Smartlead Campaign.
# Daily fresh-leads → Smartlead campaign via cron + curl curl -G https://geolayer.io/v1/leads/export \ -H "Authorization: Bearer YOUR_API_KEY" \ -d keyword=Plumber -d city=Miami -d limit=100 \ -o today.csv # then POST today.csv to Smartlead's add-leads endpoint curl -X POST https://server.smartlead.ai/api/v1/campaigns/$CAMP/leads \ -H "Authorization: Bearer $SL_KEY" \ -F "csv=@today.csv"
Zapier has a native GeoLayer app (2 triggers + 3 actions). For n8n or Make, drop in the HTTP Request node with Bearer auth.
Authorization, Header Value Bearer YOUR_API_KEYTools that generate personalized outbound first lines or research a target before writing usually need raw company + contact data. Point them at /v1/get-profile for sync enrichment or /v1/tasks/submit for batch.
For OpenAI / Anthropic tool-calling, expose GeoLayer as a function:
{
"type": "function",
"function": {
"name": "geolayer_lookup_business",
"description": "Look up verified B2B contact info for a US business",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string"},
"city": {"type": "string"},
"state": {"type": "string"}
},
"required": ["name", "city"]
}
}
}
// implementation: call POST https://geolayer.io/v1/get-profileFor embedded use cases — outbound sequencers, vertical SaaS, AI agents, or any product that pulls 25K+ leads/month through the API — we offer volume-tier pricing with SLA and white-label delivery. Email admin@geolayer.io to discuss.
| Volume / mo | $ / lead | Monthly minimum | SLA |
|---|---|---|---|
| 5K – 25K | $0.30 | $1,500 | 99.5% |
| 25K – 100K | $0.25 | $6,000 | 99.5% |
| 100K – 500K | $0.20 | $20,000 | 99.9% + on-call |
| 500K+ | custom | custom | 99.95% + signed DPA |
Standard self-serve tiers (Free / Starter / Growth / Pro / Enterprise) on the homepage pricing page remain unchanged — this section is the sales-side reference for embedded/wholesale deals.
POST https://geolayer.io/v1/tasks/submit
Initializes a new search job. Returns a submission_id immediately (HTTP 202). Use this ID to poll for results or wait for a webhook. Each unique lead returned debits one credit; failed scrapes don't burn credits.
| keyword | "Plumbers" | The business niche. Required. |
| city | "Austin" | The target location. Required. |
| max_leads | 20 | Optional cap on the number of leads to return. Defaults to 20. Limited by your remaining credits. |
Every returned lead has an email (our identity + delivery channel). Phone and address are returned when available but never gate the result set.
queued, processing, completed, out_of_credits, error.GET https://geolayer.io/v1/tasks/results/{submission_id}
Retrieves the collected data once the task is complete. If the task is still running, the status will return as "processing".
queued, processing, completed, out_of_credits, error.first_name, last_name, email, phone, company_name, title, website, address, city, state, country_code, email_verified_status). Suppression-list emails are filtered out.data is empty on a completed task, a one-line human explanation (e.g. "Filter excluded every candidate"). Otherwise null.data is empty, an object with cache_hits, fresh_fetched, must_have, and dropped_by_filter: { email, phone, address, invalid_email } counts.GET https://geolayer.io/v1/me
Legacy alias: /v1/get-user-data.
Retrieves your profile and account data.
Starter).starter, growth, pro, enterprise, or free). Legacy: scale (existing customers only).GET https://geolayer.io/v1/tasks/list-tasks
Returns your last 10 tasks (newest first). Tasks stuck in queued for more than 5 minutes are automatically retried up to 3 times before flipping to error.
{ keyword, city, max_leads } request body plus a diagnostics object once the task completes.GET https://geolayer.io/v1/get-all-leads
Every lead ever returned to your account across every task, deduped by email and filtered through your suppression list. Useful for a one-shot CRM sync.
Same row shape as Fetch Task Results.
GET https://geolayer.io/v1/leads/export?keyword=Plumbers&city=Austin&limit=1000
Streams a CSV of up to limit rows (max 10,000) for a specific keyword + city. Pulls from the cache only — does not trigger live source fetches. Debits credits atomically; if your balance can't cover limit the request returns 402 without touching the data.
| keyword | Required. |
| city | Required. |
| limit | Optional. Default 1,000. Hard cap 10,000. |
/v1/suppression
Per-account do-not-contact list. Every read path (task results, get-all-leads, bulk export, lead lists) silently filters out matching emails. Use this for opt-outs, bounces, and CAN-SPAM/GDPR removal requests.
{ "email": "..." , "reason": "user_added" } or { "emails": ["a@b.com", ...] } for bulk./v1/searches
Store and re-run searches without retyping the parameters.
{ "name": "Bay Area Plumbers", "keyword": "Plumber", "city": "San Jose" }./v1/tasks/submit and returns the same { submission_id, status } response./v1/lists
Lightweight CRM-style grouping. Create lists, add lead IDs to them, read back members for export or sync. Suppression-list filtering is applied to membership reads.
member_count.{ "name": "Top Pipeline Q3" }.{ "lead_ids": [123, 124] }./v1/webhooks
Manage your outbound webhook URL programmatically. See the Webhooks section above for the payload format and signature scheme.
{ webhook_url, signing_secret_env, signature_header, events: [...] }.{ "url": "https://your.server/hook" }. Validated as an http(s) URL.test.webhook payload to the configured URL. Response includes delivered, attempts, last_status.
curl -X POST "https://geolayer.io/v1/tasks/submit" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"keyword": "Plumbers",
"city": "San Jose",
"max_leads": 20
}'
// Response (HTTP 202)
{
"submission_id": "job_8291x_q",
"status": "queued",
"credits_remaining": 188
}
curl -X GET "https://geolayer.io/v1/tasks/results/job_8291x_q" \
-H "Authorization: Bearer YOUR_API_KEY"
// Response
{
"status": "completed",
"data": [
{
"first_name": "John",
"last_name": "Smith",
"email": "info@draindoc.com",
"phone": "+1-408-555-1234",
"company_name": "Drain Doctor",
"title": "Owner",
"website": "https://www.draindoc.com",
"address": "1234 Elm Street",
"city": "San Jose",
"state": "CA",
"country_code": "US",
"verified": true
}
]
}
curl -X GET "https://geolayer.io/v1/me" \
-H "Authorization: Bearer YOUR_API_KEY"
// Response
{
"full_name": "Tom Harrison",
"email": "tom.harrisson75@gmail.com",
"credits_remaining": 1000,
"plan_name": "Growth"
}