Ashby SDK
A Python SDK for the Ashby ATS (Applicant Tracking System) API.
Installation
pip install ashby
Or with uv:
uv add ashby
Quick Start
from ashby_sdk import AshbyClient
# Initialize client (uses ASHBY_API_KEY from environment)
client = AshbyClient()
# Or provide API key directly
client = AshbyClient(api_key="your-api-key")
# List all open jobs
jobs = client.jobs.list(status=["Open"])
for job in jobs:
print(f"{job.title} ({job.status})")
# Get candidates for a job
applications = client.applications.list(job_id=jobs[0].id)
for app in applications:
print(f"{app.candidate_name}: {app.status}")
# Get candidate details
candidate = client.candidates.get(applications[0].candidate_id)
print(f"Email: {candidate.email}")
print(f"Phone: {candidate.phone}")
# Download resume
if candidate.resume_handle:
content, filename = client.files.download(candidate.resume_handle)
with open(filename, "wb") as f:
f.write(content)
Authentication
Set your Ashby API key via environment variable:
export ASHBY_API_KEY=your-api-key
Or use a .env file (automatically loaded):
ASHBY_API_KEY=your-api-key
Required Permissions
Your API key needs these permissions:
| Module | Permission | Endpoints |
|---|---|---|
| Jobs | Read | job.list, job.info, jobPosting.list, jobPosting.info, interviewStage.list, interviewStage.info, opening.list |
| Candidates | Read | application.list, application.info, candidate.list, candidate.info, candidate.search, file.info, surveySubmission.list, applicationFeedback.list, project.list |
| Candidates | Write | candidate.createNote (for notes), candidate.addTag (for tagging), application.changeStage (for moving candidates) |
| Interviews | Read | interview.list, interview.info, interviewSchedule.list |
| Hiring Process | Read | source.list, archiveReason.list, closeReason.list, candidateTag.list, customField.list, hiringTeamRole.list |
| Organization | Read | department.list, department.info, location.list, location.info, user.list, user.info |
| Offers | Read | offer.list, offer.info |
Resources
Jobs
# List jobs (with optional status filter)
jobs = client.jobs.list()
jobs = client.jobs.list(status=["Open", "Closed"])
# Get job details
job = client.jobs.get(job_id="...")
print(job.title, job.status, job.employment_type)
print(job.hiring_team) # List of HiringTeamMember
Applications
# List applications
apps = client.applications.list()
apps = client.applications.list(job_id="...")
# Get application details
app = client.applications.get(application_id="...")
print(app.status) # "Active", "Archived", "Hired"
print(app.stage_name) # Current interview stage
print(app.candidate_name)
# Get with form submissions expanded
app = client.applications.get_with_forms(application_id="...")
for form in app.form_submissions:
parsed = client.surveys.parse_submission(form)
print(parsed["answers"])
# Move candidate to a different stage in the funnel
updated_app = client.applications.change_stage(
application_id="...",
interview_stage_id="..."
)
print(f"Moved to: {updated_app.stage_name}")
Interview Stages (Hiring Funnel)
# Get all interview stages for a job
stages = client.interview_stages.list_for_job(job_id="...")
for stage in stages:
print(f"{stage.name} (order: {stage.order_in_stage_group})")
# Or use convenience method
funnel = client.get_job_funnel(job_id="...")
print("Hiring funnel stages:")
for i, stage in enumerate(funnel, 1):
print(f" {i}. {stage.name}")
# Get current stage of an application
current_stage = client.get_application_stage(application_id="...")
print(f"Currently at: {current_stage.name}")
# Move candidate to next stage
client.move_application_to_stage(
application_id="...",
interview_stage_id=funnel[2].id # Move to 3rd stage
)
# Get details about a specific stage
stage = client.interview_stages.get(stage_id="...")
print(f"Stage: {stage.name}, Type: {stage.type}")
Candidates
# List all candidates
candidates = client.candidates.list()
# Get candidate details
candidate = client.candidates.get(candidate_id="...")
print(candidate.name)
print(candidate.email)
print(candidate.phone)
print(candidate.links) # LinkedIn, portfolio, etc.
print(candidate.tags)
print(candidate.resume_file) # File object with handle
# Search candidates by email or name
results = client.candidates.search(email="john@example.com")
results = client.candidates.search(name="John")
results = client.candidates.search(email="john@example.com", name="John") # AND
# Add a tag to a candidate
tags = client.candidate_tags.list()
if tags:
updated = client.candidates.add_tag(candidate_id="...", tag_id=tags[0].id)
Questionnaires / Form Submissions
Ashby stores form responses in two places depending on when candidates applied:
# Method 1: Application form submissions (newer candidates)
app = client.applications.get_with_forms(application_id="...")
for form in app.form_submissions:
parsed = client.surveys.parse_submission(form)
for question, answer in parsed["answers"].items():
print(f"Q: {question}")
print(f"A: {answer}")
# Method 2: Survey submissions (older candidates)
surveys = client.surveys.get_for_candidate(candidate_id="...")
for survey in surveys:
parsed = client.surveys.parse_submission(survey)
print(parsed["answers"])
# List all questionnaire submissions
all_surveys = client.surveys.list(survey_type="Questionnaire")
Files
# Get download URL
url = client.files.get_url(file_handle="...")
# Download file
content, filename = client.files.download(file_handle="...")
with open(filename, "wb") as f:
f.write(content)
# Download candidate resume
if candidate.resume_handle:
content, filename = client.files.download(candidate.resume_handle)
Job Postings (Descriptions)
# Get job posting details (includes description)
posting = client.job_postings.get(posting_id="...")
print(posting.title)
print(posting.description_plain)
print(posting.description) # Prefers plain text, falls back to stripped HTML
# Get posting for a job
posting = client.job_postings.get_for_job(job_id="...")
# Get job description directly (convenience method)
description = client.get_job_description(job_id="...")
print(description)
# List all postings for a job
postings = client.job_postings.list(job_id="...")
Notes
# Create a note on a candidate
note = client.notes.create(
candidate_id="...",
note_text="Great candidate, recommend for interview.",
note_type="text/plain" # or "text/html"
)
print(note.id)
# Or use convenience method
note = client.create_candidate_note(
candidate_id="...",
note_text="Interview scheduled."
)
# List notes for a candidate
notes = client.notes.list(candidate_id="...")
for note in notes:
print(f"{note.created_at}: {note.content}")
Interview Feedback (Scorecards)
# Get interview feedback/scorecards for an application
feedback_list = client.feedback.list_for_application(application_id="...")
for fb in feedback_list:
print(f"Submitted by: {fb.submitter.full_name}")
print(f"Recommendation: {fb.overall_recommendation}")
print(f"Technical score: {fb.get_score('Technical Skills')}")
Organization Data
# Departments
departments = client.departments.list()
dept = client.departments.get(department_id="...")
# Locations
locations = client.locations.list()
loc = client.locations.get(location_id="...")
# Users (team members)
users = client.users.list()
user = client.users.get(user_id="...")
print(user.full_name, user.email, user.global_role)
# Hiring team roles
roles = client.hiring_team_roles.list()
for role in roles:
print(role.name) # e.g., "Hiring Manager", "Recruiter"
Hiring Process Metadata
# Sources (where candidates come from)
sources = client.sources.list()
for source in sources:
print(f"{source.name} ({source.type})")
# Archive reasons (why candidates were rejected)
archive_reasons = client.archive_reasons.list()
for reason in archive_reasons:
print(f"{reason.name} - {reason.reason_type}")
# Close reasons (why jobs were closed)
close_reasons = client.close_reasons.list()
# Candidate tags
tags = client.candidate_tags.list()
# Custom field definitions
fields = client.custom_fields.list()
field = client.custom_fields.get(custom_field_id="...")
Other Resources
# Projects (talent pools)
projects = client.projects.list()
project = client.projects.get(project_id="...")
# Offers
offers = client.offers.list()
offer = client.offers.get(offer_id="...")
# Interviews
interviews = client.interviews.list()
interview = client.interviews.get(interview_id="...")
# Interview schedules
schedules = client.interview_schedules.list()
schedule = client.interview_schedules.get(interview_schedule_id="...")
Data Models
All responses are wrapped in typed dataclasses:
| Model | Description |
|---|---|
Job |
Job posting with title, status, hiring team |
JobPosting |
Job posting details with description |
Application |
Job application with status, stage, forms |
Candidate |
Candidate with contact info, resume, links |
Note |
Candidate note with content and author |
File |
File with download handle |
InterviewStage |
Pipeline stage |
Source |
Application source |
ArchiveReason |
Reason for archiving/rejecting candidates |
CloseReason |
Reason for closing jobs |
Department |
Organization department |
Location |
Office location |
User |
Team member with role |
HiringTeamRole |
Role type (Hiring Manager, Recruiter, etc.) |
Project |
Talent pool |
Offer |
Job offer |
Interview |
Scheduled interview |
InterviewSchedule |
Interview schedule |
Feedback |
Interview scorecard/feedback |
CustomFieldDefinition |
Custom field definition |
HiringTeamMember |
Hiring team member |
Tag, Link, CustomField |
Candidate metadata |
All models have a raw_data property with the complete API response.
Error Handling
from ashby_sdk import (
AshbyClient,
AshbyAPIError,
AshbyAuthError,
AshbyNotFoundError,
)
try:
client = AshbyClient()
jobs = client.jobs.list()
except AshbyAuthError as e:
print(f"Authentication failed: {e}")
except AshbyAPIError as e:
print(f"API error: {e}")
print(f"Error codes: {e.errors}")
Pagination
Pagination is handled automatically. All list() methods fetch all pages:
# This fetches ALL applications (may be thousands)
applications = client.applications.list(job_id="...")
# Adjust page size if needed
applications = client.applications.list(job_id="...", limit=50)
Development
# Clone the repo
git clone https://github.com/deepweather/ashby_python_sdk.git
cd ashby_python_sdk
# Install with dev dependencies
uv sync --all-extras
# Run tests
uv run pytest
# Run linter
uv run ruff check .
# Run type checker
uv run mypy .
API Reference
| SDK Method | Ashby Endpoint |
|---|---|
client.jobs.list() |
POST /job.list |
client.jobs.get() |
POST /job.info |
client.job_postings.list() |
POST /jobPosting.list |
client.job_postings.get() |
POST /jobPosting.info |
client.applications.list() |
POST /application.list |
client.applications.get() |
POST /application.info |
client.applications.change_stage() |
POST /application.changeStage |
client.interview_stages.list() |
POST /interviewStage.list |
client.interview_stages.get() |
POST /interviewStage.info |
client.candidates.list() |
POST /candidate.list |
client.candidates.get() |
POST /candidate.info |
client.candidates.search() |
POST /candidate.search |
client.candidates.add_tag() |
POST /candidate.addTag |
client.notes.create() |
POST /candidate.createNote |
client.notes.list() |
POST /candidate.listNotes |
client.surveys.list() |
POST /surveySubmission.list |
client.files.get_url() |
POST /file.info |
client.feedback.list_for_application() |
POST /applicationFeedback.list |
client.sources.list() |
POST /source.list |
client.archive_reasons.list() |
POST /archiveReason.list |
client.close_reasons.list() |
POST /closeReason.list |
client.departments.list() |
POST /department.list |
client.departments.get() |
POST /department.info |
client.locations.list() |
POST /location.list |
client.locations.get() |
POST /location.info |
client.users.list() |
POST /user.list |
client.users.get() |
POST /user.info |
client.candidate_tags.list() |
POST /candidateTag.list |
client.custom_fields.list() |
POST /customField.list |
client.custom_fields.get() |
POST /customField.info |
client.projects.list() |
POST /project.list |
client.projects.get() |
POST /project.info |
client.offers.list() |
POST /offer.list |
client.offers.get() |
POST /offer.info |
client.interviews.list() |
POST /interview.list |
client.interviews.get() |
POST /interview.info |
client.interview_schedules.list() |
POST /interviewSchedule.list |
client.interview_schedules.get() |
POST /interviewSchedule.info |
client.hiring_team_roles.list() |
POST /hiringTeamRole.list |
See Ashby API Documentation for full API details.
License
MIT License - see LICENSE for details.
Links
Metadata
Release files for ashby 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ashby-0.4.1.tar.gz | 29.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ashby-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 55.7 kB
Release files / ashby-0.4.1.tar.gz
| Download URL | ashby-0.4.1.tar.gz |
|---|---|
| Size | 29.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6f284859f8fe52e592e86c9f82e165d64f615238dc8f13e4d18b5184667e888f
|
|
BLAKE2b-256 checksum How to use checksums |
5c7a11deba1e17edd25103d504442d21885c92cd33f56ce603562045d0b77a51
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.2
|
Release files / ashby-0.4.1-py3-none-any.whl
| Download URL | ashby-0.4.1-py3-none-any.whl |
|---|---|
| Size | 26.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b21ef5dc6c14e3b9a01a42e16a689c9858ab275d8e5212e76b8b7b0882832d42
|
|
BLAKE2b-256 checksum How to use checksums |
d0c539045542d67e9a7cf035bfc6973043a0d40b5b5297eb1e94f330a60d3ad4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.2
|