Skip the Gatekeeper API
Base URL: {{API_BASE_URL}} · errors are { "error": { "code": "..." } } + an HTTP status

Auth

Two models, same tenant data:

Capabilities: discover:write, run:write, run:read, billing:write. A tenant token created via the dashboard carries run + billing capabilities.

Endpoints

Method + pathSession equivalentCapabilityPurpose
POST /api/runs/app/runsrun:writestart a run {keyword, location, depth?, callbackUrl?}{run_id, webhook_signing_secret?} (202)
GET /api/runs/app/runsrun:readlist 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/:idrun:readrun status + counts (total / enriched / failed / dead_letters / credits_charged / note)
GET /api/runs/:id/results.csv/app/runs/:id/results.csvrun:readstreamed CSV of the run's enriched businesses — add ?format= (below)
GET /api/locations/app/locationsrun: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/enrichrun:writeenrich 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/creditsrun:readcredit balance + recent ledger
POST /api/checkout/app/checkoutbilling:writestart a Stripe checkout for a pack → {url}
POST /api/discoverdiscover:writediscover + persist only (no enrichment)

Credits & packs

1 credit per enriched business; a permanent failure is refunded. Buy from the dashboard (/ → Buy credits):

PackCreditsPrice
starter10,000$120
growth50,000$450
scale200,000$1,400

Try it without a key

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.

CSV columns

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.

CSV formats

Add ?format= to results.csv to get a layout that imports directly into the tool you already use, instead of hand-mapping 118 columns:

formatColumnsUse for
full118default — everything, version-locked
simple7name, owner, phone, email, website, city, state
advanced21the qualification view: reachability, rating, hours, tech
ghl12GoHighLevel contact import (its own header names, auto-maps)
instantly9Instantly.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.

Agent skill

Paste this into a coding agent (Claude Code / Codex / Cursor). The base URL is filled in for this deployment.

Skip the Gatekeeper skill
# 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: /

Copied