SourceWhale is outreach shaped the way a desk works: candidates and campaigns instead of leads and deals, with reporting that matches how you are measured. Your agent pushes the shortlist in and can start the outreach once you have approved it.
SourceWhale is activation with a recruiting shape. Sourcing and enrichment fill it.
Ask for the outcome and your agent composes the run itself. Or start from a skill, a whole pipeline it already knows end to end.
browse all skills →Every tool ships with a written playbook, and the agent loads it before the first call. Auth, rate limits, what each call costs, which actions need your approval, and the mistakes worth avoiding. It is the difference between an agent that knows the tool and one that guesses at it.
Read the SourceWhale docs →Routing layer for all SourceWhale public-API work. This file is WHAT/guardrails; the HOW
lives in reference/endpoints.md and the Python client lib/sourcewhale.py.
lib/sourcewhale.py (Python-only stack).reference/endpoints.md.https://sourcewhale.app/public-apiSOURCEWHALE_API_KEY (SourceWhale Settings → API/Integrations).userEmail query param ("the user performing the
action"). Set env SOURCEWHALE_USER_EMAIL once and the client defaults every call to it;
override per-call when acting as another user./v1/... endpoints, their
query params, the candidates/search key set (socialLink|photo|email|phone), the candidate
field list, and the zapier bodies match lib/sourcewhale.py exactly. Index: reference/docs/sourcewhale/raw/endpoints.md.api-key (raw key value, no prefix) — from the Swagger
Authorize dialog (ApiKeyAuthorizer / Name: api-key / In: header). Env SOURCEWHALE_API_KEY; never hardcode.SOURCEWHALE_API_KEY is missing.Before writing/running a call, open reference/endpoints.md for the exact params, body
shape, and the candidate object fields. Don't guess field names.
SourceWhale's public API has no endpoint to create a campaign. You can only list_campaigns
(read) and add_candidates into an existing campaign. Therefore, before any candidate push:
request() or any other call — there is no such endpoint.list_campaigns(user_email=…) → find the campaign by name → use its id as
campaign_id in add_candidates. (If list_campaigns doesn't show it, the user created it under a
different userEmail — ask which user owns it.)Only after the campaign exists and you've resolved its id may you proceed to the approval-gated push below. The same applies to message/sequence steps — those are authored in the UI, not via the API.
SourceWhale is an outreach tool — writes can trigger real messages to candidates.
sendImmediately: true on add_candidates STARTS OUTREACH immediately. Treat any
call with send_immediately=True as a paid/irreversible action: confirm explicitly with
the user first. Default to send_immediately=False (or omit) unless told otherwise.sendImmediately run.modify_candidate requires a valid candidateId (get it via search_candidates).list_campaigns (optional metrics, partner filters) — read-only; no create-campaign API (see Prerequisite above: the user must create the campaign in the UI first)list_projectsadd_candidates (into campaign/projects, optional immediate send),
modify_candidate, search_candidates (by socialLink | photo | email | phone)dashboard_statistics (from/to, YYYY-MM-DD)zapier_subscribe, zapier_unsubscribeCanonical recruiting flow: source + enrich candidates (your sourcing/enrichment adapters) into
a CSV → map rows to the SourceWhale candidate object → add_candidates into a campaign. Use
search_candidates to dedupe by email before adding. Inspect the CSV first;
never read large CSVs into context.
lib/sourcewhale.pyImport: from lib.sourcewhale import SourceWhale → instantiate SourceWhale() (reads key from env). Base: https://sourcewhale.app/public-api. Generic passthrough: request(method, path, *, params, json).
add_candidates(candidates: list[dict], campaign_id: str | None = None, project_ids: list[str] | None = None, send_immediately: bool | None = None, user_email: str | None = None) -> Any — POST /v1/candidates/add — add candidates, optionally into a campaign/projects.dashboard_statistics(date_from: str, date_to: str) -> Any — GET /v1/statistics/dashboard — both dates required, format YYYY-MM-DD.list_campaigns(user_email: str | None = None, include_metrics: bool | None = None, partner: str | None = None, partner_filters: dict | None = None) -> Any — GET /v1/campaigns/list. Returns campaigns created by userEmail (if set).list_projects(user_email: str | None = None) -> Any — GET /v1/projects/list — list projects (for the user if userEmail set).modify_candidate(candidate: dict, user_email: str | None = None) -> Any — POST /v1/candidates/modify — update a candidate. candidate must include candidateId.search_candidates(key: str, value: str, user_email: str | None = None) -> Any — GET /v1/candidates/search — find candidates by an identifier.zapier_subscribe(url: str, subscription_type: str) -> Any — POST /v1/zapier/subscribe — register a webhook URL for a subscription type.zapier_unsubscribe(subscription_id: str) -> Any — POST /v1/zapier/unsubscribe — remove a webhook subscription by id.