SSpiderIQ
SSpiderIQ

Docs / api-reference/dashboard-bulk-lead-sourcing

Dashboard Bulk Lead Sourcing

10 endpoints from the published OpenAPI import.

GET/api/v1/dashboard/bulk-lead-sourcing/sources

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

StatusDescription
200Successful Response
POST/api/v1/dashboard/bulk-lead-sourcing/upload

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

StatusDescription
201Successful Response
422Validation Error
POST/api/v1/dashboard/bulk-lead-sourcing/estimate

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

StatusDescription
200Successful Response
422Validation Error
POST/api/v1/dashboard/bulk-lead-sourcing/submit

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

StatusDescription
202Successful Response
422Validation Error
GET/api/v1/dashboard/bulk-lead-sourcing/past-runs

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

NameInTypeRequiredDescription
stagesquerystringfalse
kindquerystringfalse
campaign_idqueryanyfalse
exclude_without_websitequerybooleanfalse
limitqueryintegerfalse
Try it
Query

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

StatusDescription
200Successful Response
422Validation Error
GET/api/v1/dashboard/bulk-lead-sourcing/corpus/fields

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

StatusDescription
200Successful Response
GET/api/v1/dashboard/bulk-lead-sourcing/corpus/values

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

NameInTypeRequiredDescription
fieldquerystringtrue
qqueryanyfalse
Try it
Query

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

StatusDescription
200Successful Response
422Validation Error
POST/api/v1/dashboard/bulk-lead-sourcing/corpus/count

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

StatusDescription
200Successful Response
422Validation Error
POST/api/v1/dashboard/bulk-lead-sourcing/corpus/leads

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

NameInTypeRequiredDescription
cursorqueryanyfalse
sortquerystringfalse
limitqueryintegerfalse
Try it
Query

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

StatusDescription
200Successful Response
422Validation Error
POST/api/v1/dashboard/bulk-lead-sourcing/selections

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

StatusDescription
201Successful Response
422Validation Error