- Sync and async clients (
hirebase.Client/hirebase.AsyncClient). - Typed by default — responses come back as Pydantic models; pass
return_type=dictanywhere for raw dicts. - Streaming exports — kick off an export, poll it, download it, and stream millions of jobs without loading them into memory.
- Self-contained — the SDK ships its own types and depends only on
requests,httpx, andpydantic.
Guides: Getting started · Jobs · Companies · Resumes · Tasks · Errors · Examples
Installation
pip install hirebase
# Optional extras
pip install "hirebase[streaming]" # ijson, for streaming JSON-array exports
pip install "hirebase[cli]" # the bundled `hirebase` command-line tool
Authentication
Pass your API key directly, or set it via the environment:
import hirebase
client = hirebase.Client(api_key="sk_live_...")
export HIREBASE_API_KEY="sk_live_..."
export HIREBASE_BASE_URL="https://api.hirebase.org" # optional, this is the default
Resolution order for every setting is argument → environment variable →
default. The base URL defaults to https://api.hirebase.org.
Quickstart
import hirebase
client = hirebase.Client(api_key="sk_live_...")
# Search jobs — results are typed and iterable
result = client.jobs.search({
"job_titles": ["Software Engineer", "Product Engineer"],
"locations": [{"city": "San Francisco", "region": "California",
"country": "United States"}],
"limit": 20,
})
print(result.total_count, "matches")
for job in result:
print(job.job_title, "@", job.company_name, "—", job.salary_range)
Booleans are accepted natively (visa=True), and locations is a friendly
alias for the API's geo_locations. Unknown filter keys are passed through
untouched, so new API features work before the SDK is updated.
Async
import asyncio, hirebase
async def main():
async with hirebase.AsyncClient(api_key="sk_live_...") as client:
result = await client.jobs.search({"job_titles": ["Engineer"]})
for job in result:
print(job.job_title)
asyncio.run(main())
Every method has the same signature on both clients — the async versions are awaitable.
Jobs
# Search
result = client.jobs.search(query, page=1, limit=20)
# Fetch one job
job = client.jobs.get("6958cfd211e2763c3491ef8b")
# Market insights for a cohort (same filter shape as search)
insights = client.jobs.insights({"job_titles": ["Data Scientist"]})
print(insights.headline.median_salary, insights.salary.p90)
Typed inputs
You can pass a plain dict or build a typed query:
from hirebase import JobQuery, SalaryRange
query = JobQuery(
job_titles=["Backend Engineer"],
salary=SalaryRange(min=150_000, currency="USD"),
location_types=["Remote"],
visa=True,
)
result = client.jobs.search(query)
Exporting jobs (async task flow)
Exports are processed server-side and returned as a downloadable file.
query = {
"job_titles": ["Software Engineer", "Product Engineer", "Fullstack Engineer"],
"locations": [{"city": "San Francisco", "region": "California",
"country": "United States"}],
}
# 1. Start the export -> returns a Task
task = client.jobs.export(query, format="json") # or format="csv"
# 2. Poll until it finishes -> (success, result)
success, result = client.tasks.poll(task)
if not success:
raise RuntimeError(f"Export failed: {result.error}")
# 3. Download the file (streamed to disk)
client.stream_file(result["download_url"], file_path="./jobs.json")
# 4. Stream jobs from the file (typed by default; uses constant memory)
for job in client.jobs.stream_file("./jobs.json"):
print(job.job_title)
# ...or get raw dicts
for row in client.jobs.stream_file("./jobs.json", return_type=dict):
...
poll() accepts a Task, a task dict, or a task id, plus interval,
timeout, and an on_progress callback. The result dict contains
download_url, file_size, record_count, and expiry_time.
You can also stream directly from the export URL without saving to disk (JSON Lines exports only):
for job in client.jobs.stream_url(result["download_url"]):
print(job.job_title)
Hiring manager contacts (async task flow)
Requires the hiring_manager_api feature on your key (email spencer@hirebase.org).
Research returns name, role and LinkedIn profile per contact; email and phone are
optional reveals billed only when found.
task = client.jobs.contacts("6ab32db8f1c329e432f72994") # LinkedIn only
task = client.tasks.poll(task.id, timeout=600)
for c in task.result["contacts"]:
print(c["name"], c["role"], c["profile_url"])
# Reveal one contact you picked (work email by default, phone on request)
reveal = client.jobs.reveal_contact(c["profile_url"], name=c["name"], reveal_phone=True)
reveal = client.tasks.poll(reveal.id, timeout=300)
print(reveal.result["email"], reveal.result["phone_number"])
# Or reveal every contact of a posting up front
task = client.jobs.contacts("6ab32db8f1c329e432f72994", reveal_email=True, reveal_phone=True)
Companies
# Search
companies = client.companies.search({"company_name": "Stripe"})
for company in companies:
print(company.company_name, company.company_slug)
# Get a company by slug — optionally with its jobs and live insights
company = client.companies.get("stripe", return_jobs=True, return_insights=True)
print(company.description_summary)
print(company.insights_data.headline.total_count)
# Bound helpers (the object remembers its client)
insights = company.insights()
jobs = company.get_jobs(limit=10)
# Company-scoped insights directly
insights = client.companies.insights("stripe", query={"days_ago": 30})
Typed vs. dict responses
Every method returns typed models by default. Pass return_type=dict to get
the raw API payload instead:
data = client.jobs.search(query, return_type=dict) # -> dict
job = client.jobs.get(job_id, return_type=dict) # -> dict
Errors
All errors subclass hirebase.HirebaseError:
| Exception | Meaning |
|---|---|
ConfigurationError |
No API key / bad config |
AuthenticationError |
401 — invalid API key |
PaymentRequiredError |
402 — plan/credits required |
PermissionError_ |
403 — not allowed |
NotFoundError |
404 |
RateLimitError |
429 from the request limiter (100 requests / 60 s per key); check err.retry_after |
QuotaExceededError |
429 with X-Billing-Code: limit_exceeded: the plan's included allowance is used up. Subclass of RateLimitError; err.usage holds the quota snapshot |
ServerError |
5xx |
APIError |
any other non-2xx (.status_code, .message, .body) |
TaskFailed / TaskTimeout |
export task failed or timed out |
import hirebase
try:
client.jobs.search(query)
except hirebase.RateLimitError:
...
except hirebase.APIError as e:
print(e.status_code, e.message)
Tracking quota without polling
Every metered response carries Hirebase-Usage-* headers. Pass
return_meta=True to any metered method (jobs and companies, sync or async)
to get them back with the result as a ResponseMeta:
jobs, meta = client.jobs.search({"job_titles": ["Software Engineer"]}, limit=50, return_meta=True)
usage = meta.usage # None on un-metered endpoints
print(usage.meter, usage.total_used, "/", usage.included_limit, "remaining:", usage.included_remaining)
if usage.is_meter_mode and usage.overage_used:
print("billing overage units:", usage.overage_used)
print(meta.status_code, meta.request_id) # transport details for the same call
Without the flag the return shape is unchanged, so existing code keeps working.
When a block-mode plan is at its cap the API refuses the call with a 429 that is not a rate limit, so backing off will not help:
try:
client.jobs.search(query, limit=100)
except hirebase.QuotaExceededError as err:
remaining = err.usage.included_remaining # e.g. 2 -> retry with limit=2
except hirebase.RateLimitError as err:
time.sleep(err.retry_after or 5) # the 100 req / 60 s limiter
Development
pip install -e ".[dev]"
# Offline unit tests (no network)
pytest
# Live integration tests against the real API
HIREBASE_API_KEY=sk_live_... pytest tests/test_integration.py -v
If your environment preloads conflicting pytest plugins, run with
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 pytest.
License
MIT — see LICENSE.
Release files for hirebase 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hirebase-0.2.2.tar.gz | 67.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hirebase-0.2.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 136.8 kB
Release files / hirebase-0.2.2.tar.gz
| Download URL | hirebase-0.2.2.tar.gz |
|---|---|
| Size | 67.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d3fb5b3c0e49661b168b22c57bf8cd8331ab34c07401219efad43af654da8d8b
|
|
BLAKE2b-256 checksum How to use checksums |
f22333450a44ba310c522f4ed500c773b5446ef377082dfd460389a96c059622
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / hirebase-0.2.2-py3-none-any.whl
| Download URL | hirebase-0.2.2-py3-none-any.whl |
|---|---|
| Size | 69.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9ba267a19387fb3d079194d4bc99790f1049ae16e14c56e1e8f7b3ffcd28b6b4
|
|
BLAKE2b-256 checksum How to use checksums |
a6e8ac5a9a1195614f129466d6c1b3614f2128ce753506142549a0114caec975
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log