{{API_BASE_URL}} · errors are { "error": { "code": "..." } } + an HTTP statusTwo models, same tenant data:
/api/*) — for agents / server-to-server. Send
Authorization: Bearer lb_at_.... Tokens are capability-scoped./app/*) — for the dashboard. Email/password via
/api/auth/* sets a cookie; /app/* mirrors the bearer routes.Capabilities: discover:write, run:write,
run:read, billing:write. A tenant token created via the
dashboard carries run + billing capabilities.
| Method + path | Session equivalent | Capability | Purpose |
|---|---|---|---|
| POST /api/runs | /app/runs | run:write | start a run {keyword, location, depth?, callbackUrl?} → {run_id, webhook_signing_secret?} (202) |
| GET /api/runs | /app/runs | run:read | list your runs, newest first (?limit= up to 200). Lose a run_id and this is how you find it again. |
| GET /api/runs/:id | /app/runs/:id | run:read | run status + counts (total / enriched / failed / dead_letters / credits_charged / note) |
| GET /api/runs/:id/results.csv | /app/runs/:id/results.csv | run:read | streamed CSV of the run's enriched businesses — add ?format= (below) |
| GET /api/locations | /app/locations | run:read | ?q=springfield&country=us → up to 8 ranked matches with their location_code. Free. Check before you spend — there are a lot of Springfields. |
| POST /api/enrich | /app/enrich | run:write | enrich a list you already have: {domains[], callbackUrl?} (max 10) → {run_id, domains} (202). No Maps search; poll and export exactly like a keyword run. |
| GET /api/credits | /app/credits | run:read | credit balance + recent ledger |
| POST /api/checkout | /app/checkout | billing:write | start a Stripe checkout for a pack → {url} |
| POST /api/discover | — | discover:write | discover + persist only (no enrichment) |
1 credit per enriched business; a permanent failure is refunded. Buy from the
dashboard (/ → Buy credits):
| Pack | Credits | Price |
|---|---|---|
| starter | 10,000 | $120 |
| growth | 50,000 | $450 |
| scale | 200,000 | $1,400 |
Every route has a /api/demo/* twin that needs no token and no
credits and returns fixture data. Build and test your integration before you buy:
curl "{{API_BASE_URL}}/api/demo/locations?q=austin"
curl "{{API_BASE_URL}}/api/demo/runs/demo/results.csv?format=ghl"
curl "{{API_BASE_URL}}/api/demo/runs/demo?status=running" # drive any polling state
Demo JSON always carries demo: true and CSV carries
X-Demo: true, so your test suite can assert it is on fixtures. The demo
set deliberately includes one strong lead and one weak one (no owner found,
front-desk email, site blocked the crawl) — that is what real output looks like.
118 columns (schema version 6): the core business fields, then
Mobile Number, Phone 1-5 (+ Type + Carrier),
Email 1-5 (+ Status), Page 1-5 (URL / Title / Meta / Text),
and the local-intel block — coordinates, zip, additional categories, the
1-5 star rating histogram, opening hours, price level, place / CID, and the
detected tech stack (CMS / CDN / server / ecommerce / chat / analytics /
advertising / has-blog); the email-intel block — Decision Maker Email
plus a type (owner / personal / role) for each
address; and socials split per platform with the contact and booking URLs.
Exact order is in the skill file below.
The last block is site intel: the Google Maps listing URL, a two-letter
state code, how many pages we read, and normalized schema.org data the business
publishes about itself — including Schema Founder, a name the site
declares directly rather than one inferred from prose. Its self-reported rating is kept
in its own column, never merged with Google's.
Decision Maker Email is the one to read first: the address that
reaches a person rather than a front desk. It is deliberately empty when a
business only publishes info@ — saying so is more useful than
handing you the gatekeeper and calling it a win.
Every export carries X-CSV-Schema-Version. Columns are
only ever APPENDED, so a parser reading by position is unaffected by a version bump.
Add ?format= to results.csv to get a layout that imports
directly into the tool you already use, instead of hand-mapping 118 columns:
| format | Columns | Use for |
|---|---|---|
| full | 118 | default — everything, version-locked |
| simple | 7 | name, owner, phone, email, website, city, state |
| advanced | 21 | the qualification view: reachability, rating, hours, tech |
| ghl | 12 | GoHighLevel contact import (its own header names, auto-maps) |
| instantly | 9 | Instantly.ai import, incl. a personalization merge field |
The CRM formats prefer the mobile over the listed line and the
verified email over the generic one, and split the owner into first / last.
An unknown format is a 400, never a silent fallback.
Paste this into a coding agent (Claude Code / Codex / Cursor). The base URL is filled in for this deployment.
# Skip the Gatekeeper — local-business lead discovery & enrichment
Given a niche + city, Skip the Gatekeeper searches Google Maps and enriches each business with
owner name, domain-checked emails, phone line type (mobile/landline/voip) + carrier, social
profiles, and crawled website pages, then returns a CSV.
Base URL: {{API_BASE_URL}}
## Auth
Ask the user for their Skip the Gatekeeper API token (starts with lb_at_). Send it on every call:
Authorization: Bearer lb_at_xxxxxxxx
Never print or log the token.
## Flow (async: start -> poll -> download)
1) Start a run (depth caps businesses, 1-700):
curl -s -X POST "{{API_BASE_URL}}/api/runs" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"keyword":"plumber","location":"Austin, Texas","depth":50}'
-> {"ok":true,"run_id":"01...","status":"queued"}
2) Poll until status is "done" (polling is free):
curl -s "{{API_BASE_URL}}/api/runs/RUN_ID" -H "Authorization: Bearer $API_TOKEN"
-> {"status":"running","total":50,"enriched":31,"failed":0,"credits_charged":31,...}
3) Download the CSV once done:
curl -s "{{API_BASE_URL}}/api/runs/RUN_ID/results.csv" \
-H "Authorization: Bearer $API_TOKEN" -o leads.csv
Add ?format=simple|advanced|ghl|instantly for a short, import-ready layout.
Or get JSON: GET /api/runs/RUN_ID/results?limit=100&offset=0
(page text is omitted unless you add &include=pages).
## The field to read first
"Decision Maker Email" is the address that reaches a person rather than a front desk.
It is deliberately EMPTY when the business only publishes info@ - that absence is the
answer, not a gap. "Primary Email Type" is owner | personal | role.
## Other endpoints
GET /api/runs -> list your runs, newest first (?limit= up to 200).
GET /api/locations?q=springfield -> up to 8 ranked matches with location_code. Free.
Call this first when a location is ambiguous: the wrong Springfield spends real
credits on the wrong city and looks like it worked.
POST /api/enrich -d '{"domains":["acme.com"]}' (max 10) - enrich a list you already
have, no Maps search. Returns a normal run_id.
/api/demo/* - every route above with NO token and NO credits, returning fixtures.
Build against these first. Add ?status=running to drive any polling state.
## Credits
1 credit per enriched business; permanent failures are refunded (net zero).
Check balance: GET {{API_BASE_URL}}/api/credits -> {"balance":1234,...}
If a run's note is "credit_capped", buy more credits from the dashboard.
## Webhook (skip the polling)
Pass "callbackUrl" (a public https URL) when starting a run and we POST it once when
the run reaches a terminal state (done or error) — no polling needed:
-d '{"keyword":"plumber","location":"Austin, Texas","callbackUrl":"https://you.example.com/hook"}'
-> {"ok":true,"run_id":"01...","webhook_signing_secret":""}
The POST body is the same summary as GET /api/runs/:id ({event:"run.completed", run_id,
status, total, enriched, failed, credits_charged, completed_at}). Verify authenticity:
the header "X-STG-Signature: sha256=" is HMAC-SHA256(webhook_signing_secret, raw_body).
The signing secret is stable per account and returned whenever you register a callback.
A private/loopback callbackUrl is rejected (422) — the URL must be publicly routable.
## Notes
- depth (1-700) caps the run; start small (10-50), then scale. Each enriched business = 1 credit.
- Enrichment is async — poll the run OR register a callbackUrl (above); don't expect results in the POST response.
- Errors: {"error":{"code":"..."}} with status 401 (bad token) / 403 (missing capability) / 422 (location or callback) / 429 (rate limit).
Dashboard: /