Python SDK for the Ashby ATS API
Project description
Ashby SDK
A Python SDK for the Ashby ATS (Applicant Tracking System) API.
Installation
pip install ashby-sdk
Or with uv:
uv add ashby-sdk
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ashby-0.4.0.tar.gz.
File metadata
- Download URL: ashby-0.4.0.tar.gz
- Upload date:
- Size: 28.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4a6983abf09be867a34826fdb9ccd22cec4ce620d9cf442d26f302167df80f04
|
|
| MD5 |
8de390934069e54e4e2a66a1d3a3ab14
|
|
| BLAKE2b-256 |
e371e4554df1c67e7b67be88baa750ec9e8b0cda0f51b4b921df84d0f843c7fe
|
File details
Details for the file ashby-0.4.0-py3-none-any.whl.
File metadata
- Download URL: ashby-0.4.0-py3-none-any.whl
- Upload date:
- Size: 25.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
16cd8e3d117f6ad5d4ea94328582b08c31feb2f28b0c54016693add0a93a1111
|
|
| MD5 |
868fc6c5e55c81d32279166fdccf749c
|
|
| BLAKE2b-256 |
899481502ec9d804f48163ef80397482eebe933210c133ed0b6ed5c5908c156e
|