Ashby API Documentation: A Developer's Integration Guide
The biggest mistake teams make with Ashby API documentation is treating it like a normal REST API. It isn't. Ashby uses RPC-style endpoints, POST for reads and writes, and JSON bodies everywhere, so the mental model that works for Greenhouse or a typical CRUD service will mislead you fast. If you're wiring Ashby into internal tooling, analytics, or middleware, the real work is understanding the request shape, the sync model, and the operational traps that official landing pages don't foreground.
Why Ashby's API Breaks REST Conventions
Ashby does a few things the way most backend engineers expect, but the core interface is not one of them. The endpoint shape follows /CATEGORY.method, not resource-first REST paths, and the public API uses POST for all operations, including reads. Requests also need JSON bodies with an explicit Content-Type: application/json header, which is a very different contract from a GET-heavy REST client.

What developers expect versus what Ashby actually does
A REST-trained developer usually looks for GET /candidates, GET /jobs, and maybe a cursor in a response header. Ashby instead expects something more like a method call, where the method name lives in the path and the payload lives in the body. That difference matters because client libraries, retries, caching layers, and observability tooling all tend to assume GET semantics by default.
Practical rule: treat Ashby like a remote procedure endpoint with JSON payloads, not as a browsable resource tree.
The design is operationally deliberate. Ashby's documentation and support guidance point to a backend-oriented model, and the post-only pattern reduces accidental exposure of data through query strings while standardizing server-side handling across resources. That's a better fit for recruiting systems, where integrations often move sensitive candidate and hiring data between systems that shouldn't leak into browser history or intermediary logs.
A quick comparison for your mental model
| Standard REST assumption | Ashby behavior |
|---|---|
| GET for reads | POST for reads and writes |
| Resource-based URLs | /CATEGORY.method endpoints |
| Browser-friendly defaults | JSON body, explicit content type |
| Conventional paging | Cursor-based pagination and syncToken checkpointing |
The main implementation mistake is not the transport. It's assuming the transport is ordinary, then debugging the symptoms later when caching, retries, and payload validation don't behave the way your muscle memory expects. If you've already built integrations against other ATS platforms, that's the first thing to unlearn before you go deeper.
Authentication Setup and API Key Management
Ashby uses HTTP Basic Auth with the API key as the username and an empty password. That's simple once you know it, but a lot of client helpers assume a non-empty password or hide the details behind a bearer-token abstraction. The right approach is to be explicit, especially if you're wiring it into a backend service rather than a one-off script.
Here's the shape in curl:
curl -X POST "https://developers.ashbyhq.com/..." \
-u "YOUR_API_KEY:" \
-H "Content-Type: application/json" \
-d '{"limit": 10}'
The trailing colon matters because it makes the password blank. If you're using Python or Node.js, the same idea applies, send Basic Auth credentials, not a bearer token, and don't let the library “help” you into a different authentication scheme.
Key governance matters more than token convenience
Ashby also exposes apiKey.info as an authenticated endpoint that requires apiKeysRead permission and returns the key title plus creation date. That's a useful operational signal in enterprise environments where you want to know which key is live, who issued it, and whether the integration still matches the intended service boundary. It's one of the few spots in this API surface that directly supports governance and auditability rather than just transport.
For browser-based apps, Ashby's own docs recommend proxying traffic through a backend. The long-lived key is not browser-safe, and CORS isn't enabled, so direct frontend calls are the wrong architecture even if they seem faster during prototyping. If your product needs an interactive UI, put the API call behind your server and keep the credential out of the client entirely.
Keep the key in a server-side secret store, and let the backend fan out requests on behalf of the browser. That's the only sane pattern here.
If you're building a system that sits between a user-facing app and Ashby, this is the place to design for rotation, audit logs, and least privilege from day one. The internal workflow should treat the key as infrastructure, not as application state. Connect a client in Geode uses the same basic discipline, keep credentials server-side and expose only the action surface you intend to use.
Understanding RPC-Style Endpoints and Request Patterns
Ashby's endpoint naming is easiest to read if you stop translating it into REST and let it be what it is. Methods like candidate.list, job.list, application.list, user.list, interview.list, and reportGenerate all fit the same RPC-style shape, where the method name is the unit of work. That makes the API more predictable once you accept the premise, but less intuitive if you come in expecting resource URLs and standard HTTP verbs.
Common Ashby API endpoints
| Endpoint | Purpose | Common parameters |
|---|---|---|
candidate.list |
Return candidate records | filters, cursor, limit |
job.list |
Return job records | filters, cursor, limit |
application.list |
Return application records | filters, cursor, limit |
user.list |
Return user records | cursor, limit |
interview.list |
Return interview records | filters, cursor, limit |
reportGenerate |
Generate reporting output | report config, date range |
A typical request sends JSON in the body and declares the content type explicitly. That's the contract you should build your client around, including retries, request signing, and structured logging.
curl -X POST "https://api.ashbyhq.com/candidate.list" \
-u "YOUR_API_KEY:" \
-H "Content-Type: application/json" \
-d '{
"limit": 25,
"cursor": null,
"filters": {
"status": ["active"]
}
}'
The awkward edge case is applicationForm.submit, which is called out as an exception to the application/json requirement. That's exactly the kind of special case that deserves a dedicated client wrapper, because the rest of the API strongly encourages one common request path and one common serializer. If you scatter request construction across your codebase, you'll end up with brittle adapters and inconsistent retries.
Enterprise application integration middleware patterns become useful here because Ashby behaves like a method router, not a resource server. That means your middleware should think in terms of action contracts, not endpoint discovery. Caching also needs to be selective, since POST-based reads don't fit the default assumptions that many HTTP caches make.
The cleanest client I've seen for this style of API wraps each Ashby method in a typed function and keeps transport details in one place.
That approach pays off the first time you need to change retry behavior or inspect request payloads during incident response. It also makes it easier to map internal domain language, like “list active candidates” or “generate pipeline report,” onto the API's actual method names without scattering endpoint strings through your code.
Implementing Cursor-Based Pagination and Incremental Sync
Ashby gives you two production-grade patterns for moving data without re-fetching everything every time. The first is cursor-based pagination, which is what you use when you need to walk a large result set safely. The second is incremental sync with syncToken checkpointing, which is what you use when you want to resume from the last known state instead of rebuilding the whole dataset on every run.

Cursor-based pagination in practice
The implementation pattern is straightforward. Start with an initial request, read the response cursor, then send the cursor back on the next request until the cursor is null.
curl -X POST "https://api.ashbyhq.com/job.list" \
-u "YOUR_API_KEY:" \
-H "Content-Type: application/json" \
-d '{
"limit": 100,
"cursor": null
}'
If your ingestion job crashes halfway through, store the last successful cursor before you move to the next page. That gives you a resumable job that doesn't need to replay the whole dataset. For pipelines that feed warehouse loads or internal search indexes, that's the difference between a routine rerun and a full backfill.
Incremental sync with syncToken
The syncToken pattern is the better choice when you're syncing a living dataset and want to avoid full refetches. The initial fetch gives you the base state, and the next run reuses the token so you only process what changed since the last checkpoint.
Save the checkpoint immediately after a successful batch, not after the whole job ends. That's how you avoid losing progress during a failure.
Operationally, that means your sync job should keep three pieces of state, the last cursor, the latest sync token, and a durable run marker in your own system. If a job is rate-limited or interrupted, restart from the last committed checkpoint, not from memory. That keeps your sync idempotent and makes failures boring, which is exactly what you want in HR data feeds.
The important architectural choice is simple. Use cursor pagination for breadth, and syncToken for continuity. If you confuse the two, you'll either hammer the API with unnecessary reads or lose track of what changed between runs.
Real Integration Scenarios with Complete Examples
The best Ashby integrations don't try to be clever, they do a few jobs consistently and keep state in one place. A candidate search UI, a job sync, and a reporting pipeline all fit the same client architecture if you wrap authentication, pagination, and response parsing cleanly. Integrating with Google Sheets in Geode follows the same pattern, one connector, one request shape, one place to manage state.
Candidate search that behaves like a real app
A candidate lookup flow usually starts with filters, then pages through the result set until the user stops scrolling or refining the query. Ashby's POST-only reads mean the search request belongs in the body, not the query string.
curl -X POST "https://api.ashbyhq.com/candidate.list" \
-u "YOUR_API_KEY:" \
-H "Content-Type: application/json" \
-d '{
"limit": 25,
"cursor": null,
"filters": {
"status": ["active"],
"jobId": "JOB_123"
}
}'
The UI should treat the cursor as opaque and hand it back unchanged. Don't try to interpret it, and don't stash it in a cache key that assumes GET semantics. When the response comes back, parse records into your app's shape and save the next cursor if one exists.
Job posting sync that doesn't hammer the API
For job syncs, incremental updates are usually the right default. Pull the initial set, persist the syncToken, then use that token on the next scheduled run so you only process deltas. That keeps your internal systems aligned without re-downloading the full set of jobs every time a scheduler fires.
curl -X POST "https://api.ashbyhq.com/job.list" \
-u "YOUR_API_KEY:" \
-H "Content-Type: application/json" \
-d '{
"limit": 100,
"syncToken": "LAST_SYNC_TOKEN"
}'
Your state store should own the token, not the web tier. That separation makes retries cleaner and keeps the sync process deterministic even when upstream updates are noisy.
Reporting that combines multiple endpoints
Reporting usually pulls from more than one object type. Candidates, applications, interviews, and users each contribute different slices of the same hiring story, so your reporting layer should merge them after retrieval rather than expecting one magical endpoint to do the whole job.
A sensible pattern is:
- Pull applications first.
- Resolve related candidate records.
- Join interview details.
- Attach user metadata for ownership and attribution.
- Write the normalized record into your warehouse or analytics store.
If you've built dashboards on other ATS platforms, this part will feel familiar. The main difference is that Ashby's method-style API pushes you to be explicit about each request, which is a good thing once your data pipeline grows beyond trivial volume. The extra ceremony buys you clearer failure points and better control over what gets reprocessed.
Error Handling and Debugging with Request IDs
Ashby support asks you to include the x-ashby-request-id response header when you report an issue. That's not just a support formality, it's the trace handle that turns a vague failure into a debuggable event. If you're already logging your own correlation IDs, pair them with Ashby's request ID so you can follow the full path across your systems and theirs.

What to log and when to retry
Start by distinguishing transport errors from application errors. Network timeouts, rate limiting, and temporary upstream failures belong in the retry bucket, while authentication errors and invalid parameters usually need an immediate fix in your code or configuration. Because Ashby's POST-only pattern complicates caching and observability, your logs need the payload, the endpoint method, the status, and the request ID in one place.
Practical rule: retry only when the request is safe to repeat or your client can prove idempotency.
That matters a lot for middleware and agentic systems, where the same logical action can be issued twice if a workflow resumes after a failure. If the operation changes state, make sure your job runner knows whether it already committed the upstream effect before it retries.
A support-friendly logging shape
A useful log line includes:
- Ashby method: the exact endpoint name you called.
- Request ID: the
x-ashby-request-idheader value. - Correlation ID: your internal trace value.
- Payload summary: enough to debug schema issues, without dumping secrets.
- Retry state: whether the call was first attempt or a replay.
That structure makes escalation much faster because support can search by the request ID while your own team can match it to an internal event. It also helps with long-lived sync jobs, where the failure may have occurred hours after the original request and only a good trace can connect the dots.
The last piece is validation. A surprising number of “API issues” are really payload-shape problems, especially when a POST-only API makes every request look uniform at the transport layer. Validate the body before you send it, then log the exact method name if the response still fails.
Choosing Between Full Fetch and Incremental Sync Patterns
Not every Ashby integration needs an incremental pipeline, and not every dataset deserves a full refetch on every run. The cleanest rule is to choose the lightest pattern that still preserves correctness. If the data is small and changes rarely, cursor-based full fetch is fine. If the data is large, volatile, or used in near-real-time workflows, syncToken checkpointing is the safer fit.
Full fetch works when the problem is simple
Use full fetch when the integration is mostly a lookup layer, the dataset is manageable, and the downstream consumer can tolerate a complete refresh. Candidate directories for a small team, one-off migrations, and admin tools often fit that shape. Cursor pagination gives you a predictable traversal model without the overhead of maintaining sync state across runs.
Incremental sync pays off when state matters
Use incremental sync when you care about steady updates, not just snapshots. Large candidate databases, operational dashboards, and background jobs that feed multiple internal systems all benefit from checkpointing because it limits work to the records that changed. It also makes failure recovery cleaner, since the next run can continue from the last committed state instead of restarting from zero.
The trade-off is maintenance. Incremental sync asks for durable checkpoint storage, more careful retry logic, and a bit more discipline in your job runner. Full fetch is easier to reason about on day one, but it grows expensive fast if the data set starts to churn.
A useful decision rule is simple. If your sync can safely run from scratch without hurting users or overloading your systems, start with cursor pagination. If replay cost starts to hurt, move to syncToken before the pipeline becomes your bottleneck.
Quick Reference for Ashby API Integration
Bookmark this when you're in implementation mode. Auth is HTTP Basic Auth, with the API key as the username and an empty password. Endpoints use /CATEGORY.method and POST for every operation, including reads. Headers should include Content-Type: application/json, except for the documented applicationForm.submit exception.
For data movement, use cursor-based pagination for large result sets and syncToken for incremental sync jobs. For debugging, always capture x-ashby-request-id when a call fails, because that's the trace value support will ask for. Don't use GET for reads, don't expose the key in browser code, and don't assume REST resource URLs will work just because the surface looks familiar.
If a request fails, check these first:
- Authentication: confirm the key is valid and the password is blank.
- Payload shape: verify JSON structure and required fields.
- Pagination state: check cursor or syncToken handling.
- Headers: confirm content type and request ID capture.
- Transport model: confirm your client isn't trying to cache POST as if it were GET.
For deeper implementation work, keep Ashby's official authentication, introduction, support, and sync documentation open while you build. The fastest integrations are the ones that match the API's actual contract instead of the one developers hope it has.
Geode helps when you want to keep this kind of integration knowledge, request shape, and operational playbook in one durable place instead of scattering it across assistants and chat logs. If you're standardizing API work across tools, read more at Geode and connect your assistant to a shared vault that keeps the context yours.