Skip to content
SSalesShiftDocs
API reference

Leads and enrichment

The global prospect pool, metered reveals, saved leads and conversion to contacts.

17 endpoints· 6 called live· 11 not exercised here· verified against http://127.0.0.1:8741

17 endpoints

GET/api/v1/salesshift/lead-searchesCalled liveThe organization's saved lead searches.
Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+data

Request

curl
curl 'https://api.vxcloud.io/api/v1/salesshift/lead-searches' \
  -H "Authorization: Bearer $SS_TOKEN"

Response · 200

Observed shape
{
  "success": "bool",
  "data": [
    "<empty list>"
  ]
}
Observed on 2026-08-06 — keys are real, values are the types that came back.
#get-api-v1-salesshift-lead-searchesleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:1926Called against http://127.0.0.1:8741 on 2026-08-06 with a real JWT; the response matched the shape recorded below.
POST/api/v1/salesshift/lead-searchesNot exercised hereSave a named set of pool filters as a reusable search.
Auth
get_org_user
Scope
organization
Success
201 · application/json
Envelope
success+data

Request body (required)

Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.

filtersis_sharedname

Values in the request below are typed placeholders, not sample data.

Request

curl
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/lead-searches' \
  -H "Authorization: Bearer $SS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "filters": "<value>", "is_shared": "<value>", "name": "<value>" }'

Response · 201

Top-level keys
{
  "data": "…",
  "success": "…"
}
Top-level keys read from the handler's return statements.

Errors

Statusdetail
400name required
#post-api-v1-salesshift-lead-searchesleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:1941Implemented and read in source for this inventory, but not exercised against a running server here. Treat as undemonstrated until someone calls it.
GET/api/v1/salesshift/leadsCalled liveThe organization's saved leads, optionally filtered by status.
Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+data

Query parameters

NameTypeRequiredNotes
statusstring?optional
limitintegeroptionalDefault 100. max 500.

Request

curl
curl 'https://api.vxcloud.io/api/v1/salesshift/leads' \
  -H "Authorization: Bearer $SS_TOKEN"

Response · 200

Observed shape
{
  "success": "bool",
  "data": [
    {
      "id": "str",
      "pool_person_id": "str",
      "first_name": "str",
      "last_name": "str",
      "full_name": "str",
      "title": "str",
      "seniority": "str",
      "department": "str",
      "email": "str",
      "email_status": "str",
      "email_masked": "str",
      "has_email": "bool",
      "phone": "null",
      "linkedin_url": "str",
      "phone_available": "bool",
      "company": {
        "name": "...",
        "domain": "...",
        "employee_range": "...",
        "industry": "..."
      },
      "location": "str",
      "status": "str",
      "score": {
        "value": "..."
      },
      "source": "str",
      "notes": "null",
      "tags": [
        "<empty list>"
      ],
      "owner_id": "int",
      "erasure_pending": "bool",
      "converted_contact_id": "str",
      "converted_at": "str",
      "created_at": "str"
    },
    "...x2"
  ]
}
Observed on 2026-08-06 — keys are real, values are the types that came back.
#get-api-v1-salesshift-leadsleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:1230Called against http://127.0.0.1:8741 on 2026-08-06 with a real JWT; the response matched the shape recorded below.
GET/api/v1/salesshift/leads/{lead_id}Not exercised hereOne saved lead, plus the live pool record behind it.

One saved lead, plus the live pool record behind it. The two are shown side by side deliberately: the lead is a snapshot, the pool moves on, and someone working a list needs to see when the underlying record has changed rather than discovering it after a bounce.

Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+data

Path parameters

NameTypeRequiredNotes
lead_idstring(uuid)required

Request

curl
curl 'https://api.vxcloud.io/api/v1/salesshift/leads/{lead_id}' \
  -H "Authorization: Bearer $SS_TOKEN"

Response · 200

Top-level keys
{
  "data": "…",
  "success": "…"
}
Top-level keys read from the handler's return statements.

Errors

Statusdetail
404Lead not found
#get-api-v1-salesshift-leads-lead-idleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:1039Implemented and read in source for this inventory, but not exercised against a running server here. Treat as undemonstrated until someone calls it.
PATCH/api/v1/salesshift/leads/{lead_id}Not exercised hereUpdate a saved lead's status, score, notes, disqualify reason, owner or tags.
Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+data

Path parameters

NameTypeRequiredNotes
lead_idstring(uuid)required

Request body (required)

Accepts a JSON object; the source declares no field list.

Request

curl
curl -X PATCH 'https://api.vxcloud.io/api/v1/salesshift/leads/{lead_id}' \
  -H "Authorization: Bearer $SS_TOKEN"

Response · 200

Top-level keys
{
  "data": "…",
  "success": "…"
}
Top-level keys read from the handler's return statements.

Errors

Statusdetail
404Lead not found
#patch-api-v1-salesshift-leads-lead-idleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:1292Implemented and read in source for this inventory, but not exercised against a running server here. Treat as undemonstrated until someone calls it.
POST/api/v1/salesshift/leads/{lead_id}/convertNot exercised hereLead → Contact.

Lead → Contact. The moment a record becomes mailable. Requires an email: a contact without one cannot be marketed to, and letting them through creates dead rows that quietly drag every campaign metric down. The lead row is KEPT as an audit trail, never moved.

Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+custom

Path parameters

NameTypeRequiredNotes
lead_idstring(uuid)required

Request body (optional)

Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.

lifecycle_stage

Values in the request below are typed placeholders, not sample data.

Request

curl
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/leads/{lead_id}/convert' \
  -H "Authorization: Bearer $SS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "lifecycle_stage": "<value>" }'

Response · 200

Top-level keys
{
  "already_converted": "…",
  "contact_id": "…",
  "reused_existing_contact": "…",
  "success": "…"
}
Top-level keys read from the handler's return statements.

Errors

Statusdetail
404Lead not found
400This record has been removed at the person's request and cannot be converted.
400Reveal this lead's email before converting — a contact without an address cannot be emailed.
#post-api-v1-salesshift-leads-lead-id-convertleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:1316Implemented and read in source for this inventory, but not exercised against a running server here. Treat as undemonstrated until someone calls it.
POST/api/v1/salesshift/leads/bulk-convertNot exercised hereConvert many saved leads to contacts in one call, reporting every id's outcome.
Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+custom

Request body (required)

Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.

lead_ids

Values in the request below are typed placeholders, not sample data.

Request

curl
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/leads/bulk-convert' \
  -H "Authorization: Bearer $SS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "lead_ids": "<value>" }'

Response · 200

Top-level keys
{
  "already_converted": "…",
  "converted": "…",
  "skipped_no_email": "…",
  "success": "…"
}
Top-level keys read from the handler's return statements.

Errors

Statusdetail
400lead_ids required
402computed at runtime: # entitlements.assert_contact_headroom detail = ( f"{ent.plan_name} stores up to {ent.contacts:,} contacts and this " f"workspace has {current:,}. Upgrade to add more, or remove " f"contacts you no longer need." )
#post-api-v1-salesshift-leads-bulk-convertleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:1403Implemented and read in source for this inventory, but not exercised against a running server here. Treat as undemonstrated until someone calls it.
GET/api/v1/salesshift/leads/company/{company_id}Not exercised hereA company in the pool, with the people behind it split by what this org already owns.

A company in the pool, with the people behind it split by what this org already owns. "New prospects" vs "Existing contacts" is the question someone actually has when they open an account page — they want to know what is left to work, not a raw headcount. Answering it here means one query instead of the client cross-referencing two endpoints and getting it subtly wrong.

Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+custom

Path parameters

NameTypeRequiredNotes
company_idstring(uuid)required

Request

curl
curl 'https://api.vxcloud.io/api/v1/salesshift/leads/company/{company_id}' \
  -H "Authorization: Bearer $SS_TOKEN"

Response · 200

Top-level keys
{
  "data": "…",
  "department": "…",
  "email": "…",
  "email_revealed": "…",
  "email_status": "…",
  "existing_contact_id": "…",
  "full_name": "…",
  "has_email": "…",
  "location": "…",
  "phone_available": "…",
  "phone_count": "…",
  "pool_id": "…",
  "saved_lead_id": "…",
  "score": "…",
  "seniority": "…",
  "success": "…",
  "title": "…"
}
Top-level keys read from the handler's return statements.

Errors

Statusdetail
404Company not found in the pool
#get-api-v1-salesshift-leads-company-company-idleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:926Implemented and read in source for this inventory, but not exercised against a running server here. Treat as undemonstrated until someone calls it.
POST/api/v1/salesshift/leads/convert-from-poolNot exercised herePool → Contact in one action: save, reveal if needed, convert.

Pool → Contact in one action: save, reveal if needed, convert. Without this the only route is save → switch tabs → convert, three steps for the single most common intent ("I want to email this person"). Revealing SPENDS QUOTA, so it is opt-in via `reveal_if_needed` and the response accounts for every id: nothing is silently dropped. When quota runs out mid-batch we convert what we can and say exactly how many we could not — a partial success reported as a success is how people lose trust in a meter.

Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
bare-object

Request body (required)

Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.

lifecycle_stagepool_person_idsreveal_if_needed

Values in the request below are typed placeholders, not sample data.

Request

curl
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/leads/convert-from-pool' \
  -H "Authorization: Bearer $SS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "lifecycle_stage": "<value>", "pool_person_ids": "<value>", "reveal_if_needed": "<value>" }'

Response · 200

A flat report: {converted, revealed_now, already_converted, skipped_no_quota, skipped_no_email, skipped_erased, contact_ids}.

Errors

Statusdetail
400pool_person_ids required
400Max 200 per convert — reveals are metered, so a bigger batch would spend more quota than anyone intends in one click.
#post-api-v1-salesshift-leads-convert-from-poolleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:1099Implemented and read in source for this inventory, but not exercised against a running server here. Treat as undemonstrated until someone calls it.
POST/api/v1/salesshift/leads/enrichNot exercised hereCrawl a company's own site and fold what is found back into the pool.

Crawl a company's own site and fold what is found back into the pool. This is the first thing in the product that actually WRITES to the pool, so it is where the rules the schema has been carrying finally get enforced: · **Only fills gaps.** An existing description, keyword set or address is never overwritten by a crawl. A human-curated or verified value outranks a scrape, and a "refresh" that quietly replaced good data with worse data would be impossible to notice and impossible to undo. · **Erasure is checked before every insert**, via `is_erased()`. That hook has existed since the erasure endpoint shipped and had no caller — this is it. Without this, the next crawl would resurrect exactly the people we were asked to forget. · **Provenance is recorded** in `lp_record_sources`: which URL, which fields, when. "Where did this come from" has to be answerable later. · **Nothing is invented.** A name is only derived from an address when the local part is plausibly a name; role accounts are dropped entirely; and every discovered address is `unverified`, because finding a string on a web page is not verification. Accepts a `company_id` (pool company) or a bare `domain`.

Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+custom

Request body (required)

Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.

company_iddomain

Values in the request below are typed placeholders, not sample data.

Request

curl
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/leads/enrich' \
  -H "Authorization: Bearer $SS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "company_id": "<value>", "domain": "<value>" }'

Response · 200

Top-level keys
{
  "attempted": "…",
  "changed": "…",
  "company_created": "…",
  "company_id": "…",
  "crawled": "…",
  "elapsed_ms": "…",
  "note": "…",
  "people_added": "…",
  "people_already_known": "…",
  "people_found": "…",
  "people_skipped_erased": "…",
  "status_codes": "…",
  "success": "…"
}
Top-level keys read from the handler's return statements.

Errors

Statusdetail
400company_id or domain required
502The web-research agent on your tenant node could not be reached, so nothing was crawled and nothing was changed.
404Company not found in the pool
#post-api-v1-salesshift-leads-enrichleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:1732Implemented and read in source for this inventory, but not exercised against a running server here. Treat as undemonstrated until someone calls it.
POST/api/v1/salesshift/leads/erasureNot exercised hereErase a person from the pool and flag every tenant's saved copy.

Erase a person from the pool and flag every tenant's saved copy. Three things happen, and all three are necessary: 1. The address is recorded as a HASH. We must be able to honour the block on every future crawl without retaining the address we were asked to delete — storing it in plaintext to enforce its deletion is self-defeating. 2. The pool row is deactivated rather than deleted, so a re-crawl that finds the same person does not resurrect them. Contact fields are cleared. 3. Saved leads across ALL organisations are flagged and stripped. The request is about the person, not about the tenant who happens to hold a copy, so scoping this to the caller's org would leave the data live everywhere else. Deliberately org-wide despite being called by one org's user: an erasure that only cleaned the requester's copy would be theatre.

Auth
get_org_user
Scope
organization
Success
201 · application/json
Envelope
success+custom

Request body (required)

Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.

emaillinkedin_urlnotereason

Values in the request below are typed placeholders, not sample data.

Request

curl
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/leads/erasure' \
  -H "Authorization: Bearer $SS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "email": "<value>", "linkedin_url": "<value>", "note": "<value>", "reason": "<value>" }'

Response · 201

Top-level keys
{
  "already_recorded": "…",
  "pool_rows_erased": "…",
  "saved_leads_flagged": "…",
  "success": "…"
}
Top-level keys read from the handler's return statements.

Errors

Statusdetail
400email or linkedin_url required
#post-api-v1-salesshift-leads-erasureleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:1455Implemented and read in source for this inventory, but not exercised against a running server here. Treat as undemonstrated until someone calls it.
POST/api/v1/salesshift/leads/facetsCalled liveCounts beside each filter.

Counts beside each filter. Facets are pool-wide — no tenant overlay, because the number of Directors in the pool is the same for everyone.

Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+data

Request body (required) — SearchIn

FieldTypeRequiredNotes
filtersLeadFiltersoptional
result_typestringoptionalDefault "person".
cursorstring?optional
limitintegeroptionalDefault 25.
sortSortIn?optional

The body in the request below is the exact one sent during verification.

Request

curl
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/leads/facets' \
  -H "Authorization: Bearer $SS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "filters": {} }'

Response · 200

Observed shape
{
  "success": "bool",
  "data": {
    "country": [
      {
        "value": "...",
        "count": "..."
      },
      "...x7"
    ],
    "department": [
      {
        "value": "...",
        "count": "..."
      },
      "...x4"
    ],
    "email_status": [
      {
        "value": "...",
        "count": "..."
      },
      "...x3"
    ],
    "industry": [
      {
        "value": "...",
        "count": "..."
      },
      "...x2"
    ],
    "seniority": [
      {
        "value": "...",
        "count": "..."
      },
      "...x5"
    ],
    "search_backend": "str"
  }
}
Observed on 2026-08-06 — keys are real, values are the types that came back.
#post-api-v1-salesshift-leads-facetsleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:598Called against http://127.0.0.1:8741 on 2026-08-06 with a real JWT; the response matched the shape recorded below.
GET/api/v1/salesshift/leads/pool/{pool_id}Not exercised hereEverything the pool knows about one person, plus this org's relationship to them (revealed?

Everything the pool knows about one person, plus this org's relationship to them (revealed? saved? already a contact?). Masking applies here exactly as it does in search — a detail view is not a back door around the meter.

Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+data

Path parameters

NameTypeRequiredNotes
pool_idstring(uuid)required

Request

curl
curl 'https://api.vxcloud.io/api/v1/salesshift/leads/pool/{pool_id}' \
  -H "Authorization: Bearer $SS_TOKEN"

Response · 200

Top-level keys
{
  "data": "…",
  "success": "…"
}
Top-level keys read from the handler's return statements.

Errors

Statusdetail
404Person not found in the pool
410This record has been removed at the person's request.
#get-api-v1-salesshift-leads-pool-pool-idleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:825Implemented and read in source for this inventory, but not exercised against a running server here. Treat as undemonstrated until someone calls it.
GET/api/v1/salesshift/leads/quotaCalled liveThe organization's reveal meter: used, allowance and remaining for the period.
Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+data

Request

curl
curl 'https://api.vxcloud.io/api/v1/salesshift/leads/quota' \
  -H "Authorization: Bearer $SS_TOKEN"

Response · 200

Observed shape
{
  "success": "bool",
  "data": {
    "used": "int",
    "allowance": "int",
    "remaining": "int",
    "display": "str"
  }
}
Observed on 2026-08-06 — keys are real, values are the types that came back.
#get-api-v1-salesshift-leads-quotaleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:649Called against http://127.0.0.1:8741 on 2026-08-06 with a real JWT; the response matched the shape recorded below.
POST/api/v1/salesshift/leads/revealCalled liveUn-mask one pool person's contact details; spends one metered reveal.
Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+data

Request body (required)

Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.

pool_person_id

The body in the request below is the exact one sent during verification.

Request

curl
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/leads/reveal' \
  -H "Authorization: Bearer $SS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{}'

Response · 200

Observed shape
{
  "detail": "str"
}
Observed on 2026-08-06 — keys are real, values are the types that came back.

Errors

Statusdetail
404Person not found in the pool
410This record has been removed at the person's request.
400pool_person_id required
402Reveal limit reached for this month ({...}/{...}).
#post-api-v1-salesshift-leads-revealleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:662Called against http://127.0.0.1:8741 on 2026-08-06; it returned the documented error status, not a success.
POST/api/v1/salesshift/leads/saveNot exercised hereCopy pool rows into the tenant's own list.

Copy pool rows into the tenant's own list. A SNAPSHOT, not a reference: the pool is re-crawled continuously and a saved list must not mutate under the person who qualified it.

Auth
get_org_user
Scope
organization
Success
201 · application/json
Envelope
success+custom

Request body (required)

Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.

pool_person_ids

Values in the request below are typed placeholders, not sample data.

Request

curl
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/leads/save' \
  -H "Authorization: Bearer $SS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "pool_person_ids": "<value>" }'

Response · 201

Top-level keys
{
  "already_saved": "…",
  "saved": "…",
  "success": "…"
}
Top-level keys read from the handler's return statements.

Errors

Statusdetail
400pool_person_ids required
400Max 200 leads per save
#post-api-v1-salesshift-leads-saveleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:750Implemented and read in source for this inventory, but not exercised against a running server here. Treat as undemonstrated until someone calls it.
POST/api/v1/salesshift/leads/searchCalled liveSearch the global pool.

Search the global pool. The Go node owns the query — it is the lean path over 64 partitions. This layer owns POLICY: which addresses this org has paid to see, and which rows it has already saved. The node is deliberately tenant-blind, so the overlay below is the only place an unmasked address can appear. The ORM path is a fallback for a node outage, not a second implementation to keep in step. The response says which one answered so a silent drift between them shows up in the payload instead of in a support ticket.

Auth
get_org_user
Scope
organization
Success
200 · application/json
Envelope
success+data

Request body (required) — SearchIn

FieldTypeRequiredNotes
filtersLeadFiltersoptional
result_typestringoptionalDefault "person".
cursorstring?optional
limitintegeroptionalDefault 25.
sortSortIn?optional

The body in the request below is the exact one sent during verification.

Request

curl
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/leads/search' \
  -H "Authorization: Bearer $SS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "filters": {}, "limit": 2 }'

Response · 200

Observed shape
{
  "success": "bool",
  "data": {
    "items": [
      {
        "pool_id": "...",
        "full_name": "...",
        "title": "...",
        "seniority": "...",
        "department": "...",
        "email_masked": "...",
        "email_status": "...",
        "has_email": "...",
        "phone_count": "...",
        "location": "...",
        "score": "...",
        "company": "...",
        "email_revealed": "...",
        "email": "...",
        "phone": "...",
        "linkedin_url": "...",
        "phone_available": "...",
        "saved_lead_id": "..."
      },
      "...x2"
    ],
    "next_cursor": "str",
    "result_type": "str",
    "sort": {
      "desc": "bool",
      "field": "str"
    },
    "total": "int",
    "total_display": "str",
    "total_is_estimate": "bool",
    "search_backend": "str"
  }
}
Observed on 2026-08-06 — keys are real, values are the types that came back.
#post-api-v1-salesshift-leads-searchleads_router · vxcloud_fastapiclient/app/services/salesshift/leads_router.py:314Called against http://127.0.0.1:8741 on 2026-08-06 with a real JWT; the response matched the shape recorded below.