Workable's API can pull every job, candidate, and pipeline event out of your account, but it's rate-limited to 10 requests per 10 seconds and most of it is scoped per job, not account-wide. Here's what the endpoints actually return, the limit nobody puts a number on, and a script that walks every job and exports every candidate to CSV.
- Rate limit: 10 requests per 10 seconds per token; go over it and you get a 429, not a queued retry.
- Candidates are scoped per job. There's no single "all candidates" endpoint — you list jobs, then page through each job's candidate list.
- Custom questions and custom fields are reachable, just not in the default candidate response — you fetch them from two extra endpoints per job.
- A working export script is below: walk
/jobs, page/jobs/:shortcode/candidates, write CSV.
The rate limit isn't the trap — the missing "list every candidate" endpoint is. Most write-ups on this topic either sell a unified-API middleman or point you at Workable's own reference without saying what that means for a script you write yourself.
Disclosure: Reqcore builds a competing ATS. That's why we read Workable's API docs closely enough to write this down, not a reason to take the technical detail below on faith — every claim is linked to Workable's own documentation.
What the API actually exposes
Workable's REST API is scoped by account and, for most hiring data, by job. Per Workable's own developer reference, you can pull:
- Jobs and each job's application-form questions.
- Candidates per job — profile fields, resume metadata, stage, disqualification status, source.
- Pipeline stages, scheduled events, and job/candidate activity feeds.
- Requisition data (fields, approval status, workflow) where requisitions are in use.
- Account members, recruiters, departments, and — on plans with the HR module — employees and time-tracking entries.
Every one of these is a real, documented endpoint at workable.readme.io, and Workable's own API documentation frames the whole surface around three use cases: building custom reports, exporting new hires to another system on a schedule or via webhook, and general read/write account management. Access needs a token generated from your account (Settings → API), scoped to what you're requesting — the token guide covers scopes and revocation. Pulling candidates specifically needs the r_candidates scope, and it works with any token type.
The limit nobody writes a number on: 10 requests per 10 seconds
Workable's own FAQ states it plainly: the API allows 10 requests per 10 seconds per token, and going over it returns a 429, not a soft warning or an auto-queued retry — you have to back off and retry yourself (confirmed against Workable's own API documentation, 23 July 2026). For an account with a few hundred candidates this rarely matters. For a full-history export across dozens of jobs, it's the constraint that decides whether your script finishes in two minutes or gets throttled the whole way through — which is exactly why the script below sleeps between calls instead of firing as fast as the loop allows.
Custom questions and custom fields: reachable, but not where people expect
Several existing write-ups on this topic either don't mention custom fields at all or describe them as unreachable. That's not quite right, and it's worth being precise about it because it changes how you write the export script.
Custom questions and custom fields are not included by default when you list a job's candidates. You get them in three steps: pull the job's application-form questions from /jobs/:shortcode/questions, pull the job's custom attributes from /jobs/:shortcode/custom_attributes, then request a specific candidate's full profile from /candidates/:id — the answers to both are returned there, under an answers key. So the constraint isn't that the data is inaccessible; it's that a naive "list candidates" call won't include it, and reaching it costs two extra calls per job on top of the per-candidate detail call you'd want anyway for a full export.
What "pulls every candidate" actually requires
There's no account-wide /candidates endpoint. Candidates are returned per job, from /jobs/:shortcode/candidates, capped at 100 per page (50 by default) with a paging.next URL for pagination. To export every candidate in the account, the real sequence is:
- List every job (
/jobs) to get each job'sshortcode. - For each job, page through
/jobs/:shortcode/candidatesuntilpaging.nextis empty. - Optionally, for each candidate, call
/candidates/:idto get full profile detail plus custom-question and custom-field answers. - Write the combined rows to CSV.
# export_workable_candidates.py
# Walks every job, pages every candidate, writes CSV.
# Confirm your account's API base host under Settings -> API in Workable
# before running this — it's subdomain-specific, e.g. https://{subdomain}.workable.com/spi/v3.
import csv
import os
import time
import requests
BASE_URL = os.environ["WORKABLE_API_BASE"] # e.g. https://acme.workable.com/spi/v3
TOKEN = os.environ["WORKABLE_API_TOKEN"]
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
REQUEST_GAP_SECONDS = 1.1 # keeps every token safely under 10 requests / 10 seconds
def get(url, params=None):
time.sleep(REQUEST_GAP_SECONDS)
resp = requests.get(url, headers=HEADERS, params=params)
if resp.status_code == 429:
time.sleep(10)
resp = requests.get(url, headers=HEADERS, params=params)
resp.raise_for_status()
return resp.json()
def list_jobs():
jobs, url, params = [], f"{BASE_URL}/jobs", {"state": "published"}
while url:
data = get(url, params)
jobs.extend(data.get("jobs", []))
url, params = data.get("paging", {}).get("next"), None
return jobs
def list_candidates(shortcode):
candidates, url, params = [], f"{BASE_URL}/jobs/{shortcode}/candidates", {"limit": 100}
while url:
data = get(url, params)
candidates.extend(data.get("candidates", []))
url, params = data.get("paging", {}).get("next"), None
return candidates
def main():
rows = []
for job in list_jobs():
for candidate in list_candidates(job["shortcode"]):
rows.append({
"job_title": job.get("title"),
"candidate_id": candidate.get("id"),
"name": candidate.get("name"),
"email": candidate.get("email"),
"stage": candidate.get("stage"),
"disqualified": candidate.get("disqualified"),
"created_at": candidate.get("created_at"),
})
with open("workable_candidates.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
if __name__ == "__main__":
main()
For custom questions and fields, add a /candidates/:id call inside the loop and merge the answers key into each row — expect roughly one extra request per candidate on top of the two per-job calls, so budget rate-limit time accordingly on a large account.
If you don't want to write this yourself
Unified-API vendors — Merge, Knit, Bindbee, and similar — wrap Workable's API (and a dozen other ATS APIs) behind one interface for a subscription fee. That's a reasonable trade if you're building a product that needs to sync candidates from many different ATS platforms, not just Workable. For a one-time or scheduled export from a single Workable account, the direct API and the script above cost nothing but the engineering time to run it.
If you'd rather not write code at all, how to export candidates from Workable covers the self-serve CSV report and the support-requested full export with resumes — no script required for either.
If you're pulling this data because you're leaving Workable
An API export is one path off Workable; Reqcore's migration guide covers the export-and-import path end to end, including why an unmetered import matters more than the export step itself once you're moving candidate history into a new system. Reqcore vs Workable covers the pricing-model difference if the API limits aren't the real complaint — Reqcore is our product, disclosed plainly, and it's the better fit when your bottleneck is applicant volume rather than API access. If the API isn't what you need at all, see the full list of Workable alternatives.
FAQ
Where is Workable's API documentation?
Workable's own reference lives at help.workable.com for an overview and FAQ, and the full endpoint-by-endpoint reference is at workable.readme.io.
How do I get a Workable API key?
Generate an access token from inside your Workable account, scoped to what you need (candidates, jobs, and so on). Workable's token guide covers generating and revoking tokens and the available scopes.
What is Workable's API rate limit?
10 requests per 10 seconds per token. Exceeding it returns a 429 response; there's no automatic queueing, so a script that fires requests back-to-back will get throttled — pace your calls or catch the 429 and back off, as the script above does.
Can I pull custom questions and custom fields through the Workable API?
Yes, but not from the default candidate list. Fetch a job's questions from /jobs/:shortcode/questions and its custom attributes from /jobs/:shortcode/custom_attributes, then read the actual answers from the answers key on /candidates/:id for each candidate.
Is there a working example of exporting Workable candidates via the API?
Yes — the script in this guide walks every job, pages through each job's candidates respecting the rate limit, and writes the result to CSV.