Skip to main content

Python SDK for the Ashby ATS API

Project description

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.

Links

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ashby-0.4.1.tar.gz (29.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ashby-0.4.1-py3-none-any.whl (26.0 kB view details)

Uploaded Python 3

File details

Details for the file ashby-0.4.1.tar.gz.

File metadata

  • Download URL: ashby-0.4.1.tar.gz
  • Upload date:
  • Size: 29.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for ashby-0.4.1.tar.gz
Algorithm Hash digest
SHA256 6f284859f8fe52e592e86c9f82e165d64f615238dc8f13e4d18b5184667e888f
MD5 2b5e2600dfe611a09fbfc6b410ae6d4f
BLAKE2b-256 5c7a11deba1e17edd25103d504442d21885c92cd33f56ce603562045d0b77a51

See more details on using hashes here.

File details

Details for the file ashby-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: ashby-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 26.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for ashby-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b21ef5dc6c14e3b9a01a42e16a689c9858ab275d8e5212e76b8b7b0882832d42
MD5 f822cfd59245b9ef145303b7c00bc060
BLAKE2b-256 d0c539045542d67e9a7cf035bfc6973043a0d40b5b5297eb1e94f330a60d3ad4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page