Skip to main content

Ashby SDK

PyPI version Python versions License: MIT

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.

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)

Source distribution for ashby 0.4.1
File Size Uploaded
ashby-0.4.1.tar.gz 29.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ashby 0.4.1
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.4.1 This release

2 release files

0.4.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page