Plucker API

REST and JSON. Start a search with a brief, poll it, read the shortlist. Everything you need in five requests or fewer, with any HTTP client.

Overview

The Plucker API turns a brief, written in plain English, into a ranked, scored shortlist sourced from LinkedIn. You POST a brief, get a run_id, poll it, and read the candidates when it is done.

Base URLhttps://plucker-production.up.railway.app
Content-Typeapplication/json
AuthX-API-Key header
ResponsesJSON, or CSV on export

Everything is a straight REST call. No SDK required. Any HTTP client works.

Authentication

Every call needs an X-API-Key header. Create keys under Settings → API keys. A key is shown once, at creation, so copy it then. Keys belong to your organisation, so they only ever see your organisation's searches. Rotate at any time by creating a new key and revoking the old one.

# every request carries the key
curl https://plucker-production.up.railway.app/api/scrape/{run_id} \
  -H "X-API-Key: plk_your_key_here"
Never expose a key in client-side code. Keys grant full read and write access to your organisation's runs. Store them on your server and proxy the requests.

Quickstart

Three requests to go from a brief to a shortlist. Keep the run_id from step 1 and poll step 2 until status is completed.

1. Start a search

curl -X POST https://plucker-production.up.railway.app/api/scrape \
  -H "X-API-Key: plk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "brief": "Senior Python engineers at UK SaaS scale-ups, 5+ years, ideally with AWS and Postgres.",
    "target_quality": 20,
    "min_characteristics": "Must have Python + AWS. No agencies."
  }'
Response
{
  "run_id": "8f4b1e2c-9d7a-4f6e-b3e2-1a9f8c5d3e7b",
  "status": "queued"
}

2. Poll until it completes

curl https://plucker-production.up.railway.app/api/scrape/8f4b1e2c-9d7a-4f6e-b3e2-1a9f8c5d3e7b \
  -H "X-API-Key: plk_your_key_here"

Every 5 to 10 seconds is fine. Runs typically take 30 to 60 minutes to complete, depending on target_quality. When status flips from running to completed you can read the shortlist.

3. Read the shortlist

curl https://plucker-production.up.railway.app/api/scrape/8f4b1e2c-9d7a-4f6e-b3e2-1a9f8c5d3e7b/candidates \
  -H "X-API-Key: plk_your_key_here"

Candidates come back ranked by score, highest first. Each has a LinkedIn URL, any contact details found so far, and Plucker's summary of why they matched.

POST/api/scrape

Starts a search. Returns straight away; the search runs in the background. Up to three searches run at once across the service. Anything beyond that waits in a queue and starts when a slot frees up, rather than failing.

Every brief is tidied by Format for me before the search starts, the same as on the New project screen: it is rewritten into the shape Plucker searches best, the likely employers are researched on the web, and the must-haves and places are pulled out. That takes up to a minute, and the run shows current_activity "Formatting the brief" meanwhile. Anything you send yourself (min_characteristics, target_companies, locations, name) is kept as you sent it; the formatter only fills what you left out. GET /api/scrape/{run_id} returns the formatted brief and what the search used.

Request body

FieldTypeAbout
briefRequiredstringWho you want, in plain English: a job ad, an email or a few lines all work, because it is formatted before the search. At least 10 characters.
namestringDisplay name for the project, up to 120 characters. Falls back to the first line of the brief when omitted.
target_qualityintegerHow many candidates scoring 6 or more to find before the search stops. Default 20, range 1 to 200. Bigger numbers take proportionally longer.
min_characteristicsstringMust-haves, enforced before scoring. Anyone missing them is dropped without being scored. Optional, but recommended for tight briefs.
search_modestringAccepted for compatibility and ignored. Every search uses the agent, which plans each search and adapts it to what the last one found. Any value is accepted, so existing integrations keep working.

Response · 200 OK

FieldTypeAbout
run_idstring (uuid)Use this to poll status and read candidates.
statusstringAlways "queued" at creation.

Errors

StatusCodeWhen
401unauthorizedMissing or invalid X-API-Key.
422validationbrief shorter than 10 characters, or target_quality out of range.

GET/api/scrape/{run_id}

The run's live status and progress. Poll it every 5 to 10 seconds while the run is in flight.

Response · 200 OK

Returns a Run object. The fields to watch:

FieldTypeAbout
statusstringqueued, running, completed or failed. Read the candidates once it is completed.
quality_countintegerCandidates scoring 6 or more so far. Approaches target_quality as the run progresses.
candidates_seenintegerProfiles Plucker has evaluated. Usually 3 to 5 times quality_count.
waveintegerKept for compatibility. Searches report 1; runs from before 2026-10-09 may show 2 (alumni) or 3 (adjacent roles).
current_activitystring?A readable status line, useful for a live UI.
errorstring?Only set when status is failed.

Errors

StatusCodeWhen
401unauthorizedMissing or invalid X-API-Key.
404not_foundThe run_id does not exist, or belongs to another organisation.

GET/api/scrape/{run_id}/candidates

Every candidate stored on this run so far, ranked by score, highest first. By default nothing is filtered out: you get every stored candidate, whatever their score. Safe to call before the run finishes; the list grows as more candidates come in.

Query parameters

ParameterTypeAbout
include_deletedbooleanInclude removed candidates. Default false.
min_scoreintegerOptional. Only return candidates scoring at or above this value, 1 to 10. Leave it out to get everyone.

Response · 200 OK

FieldTypeAbout
run_idstring (uuid)The run these candidates belong to.
candidatesCandidate[]Candidate objects, ranked by score, highest first.
totalintegerHow many candidates are in the list.

GET/api/scrape/{run_id}/candidates/export/csv

The shortlist as CSV, ready to import into your ATS or a spreadsheet. The same data as the JSON candidates endpoint, with the fields flattened.

Response · 200 OK

Content-Type: text/csv. The header row includes score, full_name, linkedin_url, headline, current_title, current_company, current_company_domain, location, work_email, work_email_quality, personal_email, personal_email_quality, phone_number, rf_role, rf_market, rf_specialism, rf_geographies, rf_contract_or_perm, wave and summary.

curl https://plucker-production.up.railway.app/api/scrape/{run_id}/candidates/export/csv \
  -H "X-API-Key: plk_your_key_here" \
  -o shortlist.csv

A share link is a public, read-only page for one run's shortlist. Anyone with the URL can view it without signing in. Three endpoints manage it.

POST/api/scrape/{run_id}/share

Turns sharing on. Repeating it is safe: if a link already exists you get the same one back.

curl -X POST https://plucker-production.up.railway.app/api/scrape/{run_id}/share \
  -H "X-API-Key: plk_your_key_here"
Response
{
  "share_token": "…",
  "share_url": "https://plucker.blootai.com/share/…"
}

POST/api/scrape/{run_id}/share/rotate

Issues a new link and stops the old one working. Use it when a link may have been forwarded further than intended. Same response as above.

curl -X POST https://plucker-production.up.railway.app/api/scrape/{run_id}/share/rotate \
  -H "X-API-Key: plk_your_key_here"

DEL/api/scrape/{run_id}/share

Turns sharing off. The public URL returns 404 from the next request.

curl -X DELETE https://plucker-production.up.railway.app/api/scrape/{run_id}/share \
  -H "X-API-Key: plk_your_key_here"
Response
{ "ok": true }

Errors

StatusCodeWhen
401unauthorizedMissing or invalid X-API-Key.
404not_foundThe run_id does not exist, or belongs to another organisation.

Run object

Returned by POST /api/scrape (partly) and GET /api/scrape/{run_id} (in full).

{
  "run_id": "8f4b1e2c-9d7a-4f6e-b3e2-1a9f8c5d3e7b",
  "name": "Senior Python Engineers - UK SaaS",
  "status": "running",
  "brief": "Senior Python engineers at UK SaaS scale-ups...",
  "min_characteristics": "Must have Python + AWS.",
  "target_quality": 20,
  "quality_count": 12,
  "candidates_seen": 47,
  "wave": 2,
  "current_activity": "Scoring candidate from Monzo",
  "error": null,
  "started_at": "2026-07-12T14:24:11.827Z",
  "completed_at": null,
  "created_at": "2026-07-12T14:24:09.530Z"
}

Candidate object

Returned in the candidates array. A null field means Plucker did not find that detail: either it was not on LinkedIn, or nobody has looked it up yet.

{
  "id": "c8a7f2e0-4b3d-4e2a-a1c5-9f8b7d3e4a5c",
  "linkedin_url": "https://www.linkedin.com/in/sarah-chen-99182/",
  "full_name": "Sarah Chen",
  "headline": "Staff Engineer at Monzo · Python · Distributed systems",
  "current_title": "Staff Engineer",
  "current_company": "Monzo",
  "location": "London, United Kingdom",
  "profile_picture_url": "https://media.licdn.com/...",
  "score": 9,
  "summary": "8 years' Python experience across two Series-C fintechs. Deep AWS knowledge from the Monzo migration.",
  "wave": 1,
  "work_email": "sarah.chen@monzo.com",
  "work_email_quality": "verified",
  "personal_email": null,
  "personal_email_quality": null,
  "phone_number": null,
  "rf_role": "Backend engineer",
  "rf_market": "Fintech",
  "rf_specialism": "Distributed systems",
  "rf_geographies": "United Kingdom",
  "rf_contract_or_perm": "Permanent",
  "confidence_scores": { "role_fit": 0.94, "seniority": 0.98 },
  "created_at": "2026-07-12T14:26:33.117Z"
}

Field notes

FieldTypeAbout
scoreinteger (1 to 10)How well the candidate matches your brief. Every stored candidate is returned whatever their score; pass min_score to filter.
waveintegerKept for compatibility. Candidates found since 2026-10-09 carry 1; older ones may carry 2 (alumni of matching companies) or 3 (adjacent roles).
work_email / personal_email / phone_numberstring?Contact details, looked up on demand from the dashboard. Null until someone in your team has looked them up and one was found.
*_qualitystring?The quality label returned with that address, passed through as it came. Not a fixed set of values. null when there is no address or no label.
rf_*string?Structured details in the shape RecruiterFlow uses, which Plucker extracts while scoring.

Errors

Every error comes back as JSON with a detail string.

{ "detail": "Missing X-API-Key header" }

Status codes

StatusMeaningWhen
200OKThe request succeeded.
401UnauthorizedMissing, invalid or revoked X-API-Key.
404Not FoundThe resource does not exist, or belongs to another organisation.
422UnprocessableThe body failed validation. detail names the field.
429Rate limitedMore than 60 requests in a minute on this key. Wait for the number of seconds in the Retry-After header.
500Server errorRare. Try again in about 30 seconds. If it keeps happening, tell us.

Rate limits

Plucker rate-limits per API key, not per organisation.

LimitValueAbout
Requests a minute60Per API key. Beyond that you get a 429 with a Retry-After header.
Concurrent runs3Across the whole service, not per organisation. Runs started beyond that queue and start when a slot frees up. They do not fail.

Hit a 429? The Retry-After response header tells you the wait in seconds, and simple exponential backoff works. There are no quotas on candidates or contact lookups.

Changelog

Briefs are formatted automatically. Every brief sent to POST /api/scrape goes through Format for me before the search starts. Fields you send explicitly are kept. The run object now includes target_companies and locations, and name is filled in.
Every search uses the agent. search_mode on POST /api/scrape is now ignored: the "refined" and "direct" modes are gone, and every search is planned and adapted by the agent. The field is still accepted, whatever its value, so existing integrations keep working; it no longer returns a 400.
Docs corrected and share links documented. The rate limit is 60 requests a minute per API key, with 429 and Retry-After. Runs beyond 3 at once queue rather than fail. The candidates list returns every stored candidate by default; min_score is an optional filter. *_quality values are passed through as they came. There are no personal email quotas. The share link endpoints (POST /share, POST /share/rotate, DELETE /share) are now documented. POST /api/scrape accepts optional name and search_mode. Candidate objects include phone_number; the run object includes name.
Multi-tenant. Every run and API key is now scoped to the calling organisation. If you rotate keys, only your organisation's data comes back.
New endpoint. GET /api/scrape/{run_id}/candidates/export/csv for a one-shot CSV download.
Public API launch. Everything under /api/scrape/* is stable and versioned in place.

Something missing?

Email jimmy@blootai.com with what you'd like to see in the docs, or with an issue you're hitting on the API. Fast turnaround.

Email jimmy@blootai.com