Dashboard Bulk Lead Sourcing
10 endpoints from the published OpenAPI import.
List Bulk Sources
Every source the dropdown should show, with availability resolved live.
Availability comes from the adapter registry rather than a hardcoded list, so the UI cannot claim a provider the backend would refuse — and does not need a frontend change on the day a new adapter lands.
ADMIN-ONLY sources are OMITTED, not disabled. _ROADMAP_SOURCES below renders a not-built-yet capability greyed out precisely so it is visible before it works; for a source whose client exposure is being withheld that is the opposite of what is wanted — a greyed-out entry advertises it to every tenant. The same filter runs on the submit path (prepare_submission), because both /estimate and /submit resolve the adapter by name from the body: a filtered dropdown is a menu, not a lock.
⚠️ No adapter is admin_only today — sortlist, the flag's first and so far only user, was opened to clients by the owner ruling of 2026-08-18. The filter is therefore live code with no production subject, which is exactly the state in which a "simplification" deletes it and nothing goes red. test_sortlist_client_exposure registers a throwaway admin-only adapter to keep the control real.
The role comes from the authenticated session via the canonical dep — never from a header, query param or body field (Dashboard RBAC Rule 7).
Try it
Examples
cURL
curl -X GET 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/sources' \
-H 'Authorization: Bearer <token>'Python
import httpx
resp = httpx.get(
"https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/sources",
headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())JavaScript
const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/sources", {
method: "GET",
headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);Go
package main
import (
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/sources", nil)
req.Header.Set("Authorization", "Bearer <token>")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}Responses
| Status | Description |
|---|---|
200 | Successful Response |
Upload Bulk Source File
Store a records file. Nothing is enriched, nothing is queued, nothing is spent.
THIS IS NOT A SUBMIT, AND THE SPLIT IS THE POINT. An upload is free and reversible: the file sits in storage, the client can abandon it, and the expiry sweep removes it. The run — and the downstream enrichment spend that comes with it — starts only at /submit, behind the confirmation. Combining the two would mean picking a file is buying the enrichment, with no screen in between, which is precisely the accident the bulk confirmation exists to prevent.
provider/source_kind are recorded at UPLOAD time rather than inferred at submit, so a file uploaded as CSV cannot be submitted as JSON. The parsers differ, and that mismatch would otherwise surface as an unreadable artifact several stages downstream instead of a 422 at the door.
The 64 MiB cap is enforced mid-stream, from the bytes actually received — never from Content-Length or UploadFile.size, both of which are supplied by the party being limited. Note also that host nginx caps the request body BEFORE this handler runs; the scoped location in apps/web/nginx-app.spideriq.ai.conf is set above the app's cap on purpose, so an over-size upload gets this clean JSON 413 rather than nginx's raw HTML page (fastapi/LEARNINGS.md, press 3.1).
Try it
Examples
cURL
curl -X POST 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/upload' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"provider": "string",
"source_kind": "string",
"file": "string"
}'Python
import httpx
resp = httpx.post(
"https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/upload",
headers={"Authorization": "Bearer <token>", "Content-Type": "application/json"},
json={"provider": "string", "source_kind": "string", "file": "string"},
)
resp.raise_for_status()
print(resp.json())JavaScript
const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/upload", {
method: "POST",
headers: { "Authorization": "Bearer <token>", "Content-Type": "application/json" },
body: JSON.stringify({"provider": "string", "source_kind": "string", "file": "string"})
});
const data = await resp.json();
console.log(data);Go
package main
import (
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"provider": "string", "source_kind": "string", "file": "string"}`)
req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/upload", body)
req.Header.Set("Authorization", "Bearer <token>")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}Responses
| Status | Description |
|---|---|
201 | Successful Response |
422 | Validation Error |
Estimate Bulk Lead Sourcing
Size the run WITHOUT committing to it.
Writes nothing, consumes no quota and creates no dispatch_decisions row. Safe to call on every keystroke; the frontend debounces it anyway.
⚠️ It may now READ from the provider (card SDS-19, 2026-08-20) — it no longer "contacts no provider", and the weaker claim is the honest one. Sortlist's spec_probe issues one header-only GET against the public listing URL for the chosen pair, so a client is told that advertising x germany-de has no page BEFORE the confirm button rather than by a failed run afterwards. No credentials, no order, no spend, nothing written — and the verdict is cached, so a debounced form does not re-probe on every keystroke.
would_exceed_cap is advisory only. The authoritative refusal is still the dispatcher's at submit time — this is the same comparison run early so the user finds out before the confirm button, not after it.
Try it
Examples
cURL
curl -X POST 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/estimate' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"source": {
"provider": "string",
"source_kind": "string",
"queries": [
"string"
],
"upload": {
"upload_id": "string"
},
"selection": {
"selection_id": "string"
},
"geo": [
{
"label": "string",
"latitude": 0.0,
"longitude": 0.0,
"country_code": "string",
"region": "string"
}
],
"limits": {
"max_records_per_query": 0,
"max_total_records": 0
},
"filters": {},
"language": "en"
},
"settings": {
"workflow": {
"spidersite": {
"enabled": {},
"mode": {},
"max_pages": {},
"crawl_strategy": {},
"target_pages": {},
"enable_spa": {},
"spa_timeout": {},
"extract_team": {},
"extract_company_info": {},
"extract_pain_points": {},
"product_description": {},
"icp_description": {},
"compendium": {},
"timeout": {}
},
"spiderverify": {
"enabled": {},
"check_gravatar": {},
"check_dnsbl": {},
"smtp_timeout_secs": {},
"max_emails_per_business": {}
},
"vayapin": {
"enabled": {}
},
"social_media_enrichment": {
"enabled": {}
},
"smartlead": {
"enabled": {},
"connection_id": {},
"remote_campaign_id": {},
"remote_campaign_name": {},
"limit": {},
"only_with_vayapin_seo": {},
"only_with_vayapin_pin": {},
"field_map": {}
},
"filter_social_media": true,
"filter_review_sites": true,
"filter_directories": true,
"filter_maps": true
}
},
"priority": 5,
"test": false
}'Python
import httpx
resp = httpx.post(
"https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/estimate",
headers={"Authorization": "Bearer <token>", "Content-Type": "application/json"},
json={"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false},
)
resp.raise_for_status()
print(resp.json())JavaScript
const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/estimate", {
method: "POST",
headers: { "Authorization": "Bearer <token>", "Content-Type": "application/json" },
body: JSON.stringify({"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false})
});
const data = await resp.json();
console.log(data);Go
package main
import (
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false}`)
req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/estimate", body)
req.Header.Set("Authorization", "Bearer <token>")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}Responses
| Status | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
Submit Bulk Lead Sourcing From Dashboard
Start the run. Same gate, same manifest, same response as the Bearer door.
The only differences are how the caller proved who they are and which client_id that resolved to. token_id is left None: per-token plan scoping is a PAT concept, and a cookie session has no token to scope by — the dispatcher then falls through to the client-level plan, which is correct.
Try it
Examples
cURL
curl -X POST 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/submit' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"source": {
"provider": "string",
"source_kind": "string",
"queries": [
"string"
],
"upload": {
"upload_id": "string"
},
"selection": {
"selection_id": "string"
},
"geo": [
{
"label": "string",
"latitude": 0.0,
"longitude": 0.0,
"country_code": "string",
"region": "string"
}
],
"limits": {
"max_records_per_query": 0,
"max_total_records": 0
},
"filters": {},
"language": "en"
},
"settings": {
"workflow": {
"spidersite": {
"enabled": {},
"mode": {},
"max_pages": {},
"crawl_strategy": {},
"target_pages": {},
"enable_spa": {},
"spa_timeout": {},
"extract_team": {},
"extract_company_info": {},
"extract_pain_points": {},
"product_description": {},
"icp_description": {},
"compendium": {},
"timeout": {}
},
"spiderverify": {
"enabled": {},
"check_gravatar": {},
"check_dnsbl": {},
"smtp_timeout_secs": {},
"max_emails_per_business": {}
},
"vayapin": {
"enabled": {}
},
"social_media_enrichment": {
"enabled": {}
},
"smartlead": {
"enabled": {},
"connection_id": {},
"remote_campaign_id": {},
"remote_campaign_name": {},
"limit": {},
"only_with_vayapin_seo": {},
"only_with_vayapin_pin": {},
"field_map": {}
},
"filter_social_media": true,
"filter_review_sites": true,
"filter_directories": true,
"filter_maps": true
}
},
"priority": 5,
"test": false
}'Python
import httpx
resp = httpx.post(
"https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/submit",
headers={"Authorization": "Bearer <token>", "Content-Type": "application/json"},
json={"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false},
)
resp.raise_for_status()
print(resp.json())JavaScript
const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/submit", {
method: "POST",
headers: { "Authorization": "Bearer <token>", "Content-Type": "application/json" },
body: JSON.stringify({"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false})
});
const data = await resp.json();
console.log(data);Go
package main
import (
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false}`)
req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/submit", body)
req.Header.Set("Authorization", "Bearer <token>")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}Responses
| Status | Description |
|---|---|
202 | Successful Response |
422 | Validation Error |
List Bulk Past Runs
The past runs this tenant could enrich, each with its label and its two counts.
Reads only. Creates no selection, queues nothing, spends nothing — picking a run is free and only the submit is not.
stages is a comma-separated list of the stages the NEW run would enable, and every eligible_leads is scoped to it (DECISION #2, design §3.3). It is REQUIRED rather than defaulted: a default stage set would silently answer a question the user did not ask, and the answer is a number they are about to size a purchase against.
kind=job lists source jobs instead of campaigns. Inside a campaign (campaign_id=…) that is §7's escape hatch — a campaign whose eligible count is over the ceiling is split by picking one of its locations. Outside one it is how the standalone lead searches are reached, which for some tenants is most of the corpus.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
stages | query | string | false | |
kind | query | string | false | |
campaign_id | query | any | false | |
exclude_without_website | query | boolean | false | |
limit | query | integer | false |
Try it
Examples
cURL
curl -X GET 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/past-runs' \
-H 'Authorization: Bearer <token>'Python
import httpx
resp = httpx.get(
"https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/past-runs",
headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())JavaScript
const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/past-runs", {
method: "GET",
headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);Go
package main
import (
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/past-runs", nil)
req.Header.Set("Authorization", "Bearer <token>")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}Responses
| Status | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
List Corpus Fields
The queryable field catalogue (§6.2). The UI builds every picker from this.
Static — the same for every tenant, because it describes the SHAPE of the corpus, not its contents. Adding a filterable field is therefore an API deploy and never a dashboard rebuild, which is the whole point of serving it rather than hardcoding it client-side.
Tenant-gated even though the payload carries no tenant data: it is a map of what can be asked, and an unauthenticated map of our schema is free reconnaissance.
Try it
Examples
cURL
curl -X GET 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/fields' \
-H 'Authorization: Bearer <token>'Python
import httpx
resp = httpx.get(
"https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/fields",
headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())JavaScript
const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/fields", {
method: "GET",
headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);Go
package main
import (
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/fields", nil)
req.Header.Set("Authorization", "Bearer <token>")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}Responses
| Status | Description |
|---|---|
200 | Successful Response |
List Corpus Field Values
Distinct values for ONE enumerable field, from THIS tenant's own corpus.
field is a catalogue key, not a column: it is looked up, and an unknown or non-enumerable key is a 422. So although this endpoint reads real tenant data, there is no request field through which a caller can name a column, a table or a schema.
Bounded hard (LIMIT), and only offered for fields the catalogue marks enumerable — a DISTINCT over free text on a six-figure table is a scan per keystroke.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
field | query | string | true | |
q | query | any | false |
Try it
Examples
cURL
curl -X GET 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/values' \
-H 'Authorization: Bearer <token>'Python
import httpx
resp = httpx.get(
"https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/values",
headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())JavaScript
const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/values", {
method: "GET",
headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);Go
package main
import (
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/values", nil)
req.Header.Set("Authorization", "Bearer <token>")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}Responses
| Status | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
Count Corpus Leads
Both counts for a filter, WITHOUT saving anything (§6.5).
This is the endpoint behind the live counter, so it is the one that fires most often. Two disciplines make that affordable and they belong on opposite sides of the wire:
- the CLIENT debounces ~300 ms and cancels the in-flight request rather than queueing it — a user typing "pizzeria" must not leave 8 counts running (§11.4);
- the SERVER bounds each one with
SET LOCAL statement_timeout, so a legal-but- pathological AST that the structural limits let through cannot pin a core.
Neither substitutes for the other: cancelling a request does not stop the query it already started, and a timeout does not stop eight of them being started.
Try it
Examples
cURL
curl -X POST 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/count' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"source_kind": "corpus_query",
"campaign_id": "string",
"job_id": "string",
"stages": [
"string"
],
"exclude_without_website": true,
"filter": {}
}'Python
import httpx
resp = httpx.post(
"https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/count",
headers={"Authorization": "Bearer <token>", "Content-Type": "application/json"},
json={"source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}},
)
resp.raise_for_status()
print(resp.json())JavaScript
const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/count", {
method: "POST",
headers: { "Authorization": "Bearer <token>", "Content-Type": "application/json" },
body: JSON.stringify({"source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}})
});
const data = await resp.json();
console.log(data);Go
package main
import (
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}}`)
req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/count", body)
req.Header.Set("Authorization", "Bearer <token>")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}Responses
| Status | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
Browse Corpus Leads
One page of matching leads, keyset-paginated (§11.5).
POST rather than GET because the filter is a tree and a tree does not belong in a query string; cursor/sort/limit stay in the query string because they are the page controls, not the question.
🔴 No page NUMBER, and no OFFSET anywhere in the generated SQL. Measured, LIMIT 50 OFFSET 100000 costs 177 ms and grows with depth; a keyset walk makes page 2,000 cost what page 1 costs. The cursor is opaque, and it is decoded and BOUND, never trusted.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
cursor | query | any | false | |
sort | query | string | false | |
limit | query | integer | false |
Try it
Examples
cURL
curl -X POST 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/leads' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"source_kind": "corpus_query",
"campaign_id": "string",
"job_id": "string",
"stages": [
"string"
],
"exclude_without_website": true,
"filter": {}
}'Python
import httpx
resp = httpx.post(
"https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/leads",
headers={"Authorization": "Bearer <token>", "Content-Type": "application/json"},
json={"source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}},
)
resp.raise_for_status()
print(resp.json())JavaScript
const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/leads", {
method: "POST",
headers: { "Authorization": "Bearer <token>", "Content-Type": "application/json" },
body: JSON.stringify({"source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}})
});
const data = await resp.json();
console.log(data);Go
package main
import (
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}}`)
req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/leads", body)
req.Header.Set("Authorization", "Bearer <token>")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}Responses
| Status | Description |
|---|---|
200 | Successful Response |
422 | Validation Error |
Create Bulk Corpus Selection
Create a selection over THIS tenant's own lead corpus, and size it.
201 with the selection id and the number of leads that would actually gain something. The submit then references the id alone — the filter is never sent on submit (design §2.2), which is what keeps a predicate that could name another tenant's data off the wire entirely.
The count returned is Gate 2's, not the filter's: leads that will gain something from the requested stages. It is what the confirmation screen must show, because a run is sized in enrichment volume (leads x stages) and the filter's own count would over-state it — §6.5's "select 5,000, enrich 200".
The tenant comes from the authenticated session via the canonical dep, and the corpus schema is derived from THAT client_id alone. There is no request field through which a caller can name a schema, a tenant, or a table.
Try it
Examples
cURL
curl -X POST 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/selections' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"source_kind": "string",
"campaign_id": "string",
"job_id": "string",
"stages": [
"string"
],
"exclude_without_website": true,
"filter": {}
}'Python
import httpx
resp = httpx.post(
"https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/selections",
headers={"Authorization": "Bearer <token>", "Content-Type": "application/json"},
json={"source_kind": "string", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}},
)
resp.raise_for_status()
print(resp.json())JavaScript
const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/selections", {
method: "POST",
headers: { "Authorization": "Bearer <token>", "Content-Type": "application/json" },
body: JSON.stringify({"source_kind": "string", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}})
});
const data = await resp.json();
console.log(data);Go
package main
import (
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"source_kind": "string", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}}`)
req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/selections", body)
req.Header.Set("Authorization", "Bearer <token>")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}Responses
| Status | Description |
|---|---|
201 | Successful Response |
422 | Validation Error |