Plain-language job search for AI apps
Pass a user's sentence straight to JobsPipe from a chatbot or agent and get back ranked postings with a relevance score.
Your chatbot's user types "senior backend engineer in Berlin, visa sponsorship, hybrid ok". Send that sentence to agentic search: it plans the searches, runs them, reads each posting for the conditions asked about, and returns the best matches first with a relevance score. When your code already knows the filters, jobs search is faster and pages through the whole result set.
Send the user's request
curl https://api.jobspipe.dev/v1/jobs/agentic-search \
-H "Authorization: Bearer jp_live_your_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5b2f0c1e-8a7d-4c1b-9f3e-2d6a4b8c0e11" \
-d '{
"query": "senior backend engineer in Berlin, visa sponsorship, hybrid ok",
"filters": { "posted_at_max_age_days": 30 },
"limit": 10
}'import uuid
import requests
resp = requests.post(
"https://api.jobspipe.dev/v1/jobs/agentic-search",
headers={
"Authorization": "Bearer jp_live_your_key_here",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"query": "senior backend engineer in Berlin, visa sponsorship, hybrid ok",
"filters": {"posted_at_max_age_days": 30},
"limit": 10,
},
timeout=60,
)
resp.raise_for_status()
result = resp.json()
print(result["metadata"]["agentic"]["intent"])
for job in result["data"]:
print(job["relevance"], job["job_title"], "-", job["company"])const resp = await fetch("https://api.jobspipe.dev/v1/jobs/agentic-search", {
method: "POST",
headers: {
Authorization: "Bearer jp_live_your_key_here",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
query: "senior backend engineer in Berlin, visa sponsorship, hybrid ok",
filters: { posted_at_max_age_days: 30 },
limit: 10,
}),
signal: AbortSignal.timeout(60_000),
});
const result = await resp.json();
console.log(result.metadata.agentic.intent);
for (const job of result.data) {
console.log(job.relevance, job.job_title, "-", job.company);
}The body takes three fields:
| Field | What to put in it |
|---|---|
query | The user's words, 2 to 500 characters. Pass them as they are. |
filters | Hard rules your app already knows, in filter names: job_country_code_or, city_or, remote, posted_at_max_age_days and the rest. Use it for things the user did not say but your product requires, such as "only the last 30 days". Paging and ordering keys are not accepted. |
limit | 1 to 25 postings, default 10. |
An Idempotency-Key makes a retry safe: the same request with the same key within 24 hours returns the same page without planning again.
Read the result
{
"metadata": {
"total_results": null,
"next_cursor": null,
"credits_charged": 8,
"jobs_already_paid": 2,
"credits_remaining": 24382,
"credits_allowance": 25000,
"agentic": {
"intent": "Senior backend engineer in Berlin, hybrid acceptable, with visa sponsorship.",
"role_intent": "Senior backend engineer",
"rounds": 1,
"stop": "enough candidates passed",
"candidates": 96,
"passed": 31,
"fallback_used": false,
"judge": [{ "key": "visa", "question": "Does the posting say the employer sponsors work visas?", "want": "yes", "hard": false }]
}
},
"data": [
{
"id": "8164933",
"job_title": "Senior Backend Engineer (Go)",
"company": "Northwind Labs",
"location": "Berlin, Germany",
"country_code": "DE",
"source_url": "https://careers.northwind.example/jobs/8164933",
"relevance": 0.91,
"uncertain": false,
"judgments": { "relevance": 0.96, "constraints": { "visa": { "yes": 0.82, "no": 0.02, "not_stated": 0.16 } } },
"lanes": ["Senior backend Berlin"]
}
]
}datais ranked, highestrelevance(0 to 1) first. Postings below 0.3 are never returned, so a short list means nothing better exists.uncertain: truemarks a role match between 0.3 and 0.7. Show it lower or label it "might be a fit".judgments.constraintsgives, per condition, how likely the posting answers yes, no or not stated. Use it to explain a match: "Mentions visa sponsorship".metadata.agentic.intentrestates the request. Echo it back ("Looking for: ...") so the user can correct it.total_resultsandnext_cursorare alwaysnull. The page is the whole answer.
Give it to an agent as a tool
Most agent frameworks take a function with a docstring. Return a compact result: the model needs a title, company, link and score, not the full job object.
import uuid
import requests
def search_jobs(request: str, country_codes: list[str] | None = None) -> list[dict]:
"""Find live job postings that match a plain-language request.
request: what the user is looking for, in their own words.
country_codes: optional ISO country codes to restrict to, e.g. ["DE"].
"""
filters = {"posted_at_max_age_days": 30}
if country_codes:
filters["job_country_code_or"] = country_codes
resp = requests.post(
"https://api.jobspipe.dev/v1/jobs/agentic-search",
headers={
"Authorization": "Bearer jp_live_your_key_here",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"query": request, "filters": filters, "limit": 10},
timeout=60,
)
if resp.status_code == 503:
return [{"error": "Agentic search is off; fall back to /v1/jobs/search."}]
resp.raise_for_status()
return [
{
"title": job["job_title"],
"company": job["company"],
"location": job.get("location"),
"url": job.get("source_url"),
"relevance": job["relevance"],
"uncertain": job["uncertain"],
}
for job in resp.json()["data"]
]Building on an MCP-capable framework or client (Claude, ChatGPT, Cursor, a custom host)? The JobsPipe MCP server gives the agent search tools directly, with OAuth sign-in, so you do not need to wrap the HTTP API yourself.
Timeouts, limits and cost
- Typical latency is 5 to 15 seconds, up to about 30. Set client timeouts to 60 seconds and show a "searching..." state.
- 10 calls a minute per account; over it you get
429withRetry-After: 60. - 1 credit per posting returned that you have not already paid for this month. Candidates not returned cost nothing.
503means agentic search is switched off; fall back to jobs search.