ashby-ts
v0.1.1
Published
Typed TypeScript client for the Ashby API, generated from Ashby's OpenAPI spec. Every endpoint, fully typed, with async-iterator pagination and incremental sync.
Maintainers
Readme
ashby-ts
Typed TypeScript client for the Ashby API, generated from Ashby's published OpenAPI spec.
All 173 endpoints across 53 resource namespaces, fully typed. Zero runtime dependencies. Works in Node 18+, Bun, and edge runtimes (it only needs fetch and btoa).
npm install ashby-ts
# or
bun add ashby-tsQuick start
import { Ashby } from "ashby-ts";
const ashby = new Ashby({ apiKey: process.env.ASHBY_API_KEY! });
// every list endpoint is an async iterator; pagination is handled for you
for await (const job of ashby.job.list({ status: ["Open"] })) {
console.log(job.title);
}
const { results: candidate } = await ashby.candidate.info({ id: "..." });The client surface mirrors Ashby's own API exactly: their endpoints are RPC-style noun.verb calls (application.list, candidate.create), so the client is ashby.application.list(), ashby.candidate.create(), and so on for every endpoint in the spec. If you know their API reference, you know this client. Responses come back as Ashby's own envelopes, typed.
Pagination
Every cursor-paginated endpoint returns a lazy call object. Awaiting it fetches one page. Iterating it fetches everything, following nextCursor until the data runs out.
// one page
const page = await ashby.application.list({ limit: 50 });
page.results; // Application[]
page.nextCursor; // string | undefined
// or the explicit spelling of the same thing
const samePage = await ashby.application.list({ limit: 50 }).page();
// every item, across every page
for await (const app of ashby.application.list({ jobId })) {
// breaking early stops fetching; no wasted requests
}
// collect everything into one array
const all = await ashby.application.list({ jobId }).all();Incremental sync
Ashby's list endpoints support syncToken incremental sync, and their docs push it hard for good reason: it's the difference between refetching your whole ATS and fetching what changed. The sync() helper drains all pages and hands you the next token.
// first sync: everything
let { results, syncToken } = await ashby.candidate.sync();
// later syncs: only what changed
({ results, syncToken } = await ashby.candidate.sync({ syncToken }));sync() exists on the 26 namespaces whose list endpoint supports it. The types won't let you call it anywhere else.
Errors
Ashby reports most failures as HTTP 200 with { success: false, errors: [...] }. The client unwraps that: a successful call resolves with the success envelope, anything else throws AshbyError carrying Ashby's actual error details.
import { AshbyError } from "ashby-ts";
try {
await ashby.application.changeStage({ applicationId, interviewStageId });
} catch (err) {
if (err instanceof AshbyError) {
err.status; // HTTP status (200 for logical errors)
err.endpoint; // "application.changeStage"
err.errors; // [{ message, parameter? }]
}
}429s and 5xxs are retried twice with backoff, honoring Retry-After. Tune with maxRetries.
Escape hatch
If you'd rather call endpoints by path, request() is the raw typed transport:
const res = await ashby.request("/apiKey.info");How it's built
The entire client is generated from Ashby's spec; there is no per-endpoint code to drift out of date. Specs run through this package end to end: one comes in, one goes out.
Ashby's OpenAPI spec spec/ashby-api.json (vendored, 173 endpoints)
-> openapi-typescript src/generated/api.ts (types only, zero runtime)
-> mapped types + Proxy the client (~200 lines of runtime, no per-endpoint code)
-> @openpkg-ts/sdk openpkg.json + docs/api.md (this package's own API, extracted from its types)openapi-typescriptturns the vendored spec into types- the client surface is a mapped type over those generated types: namespaces, methods, request bodies, response envelopes, which endpoints paginate, which support sync
- the runtime is one small Proxy-based fetch wrapper, because Ashby's API is uniform enough that nothing else is needed
openpkg.jsonis this package's own API spec, extracted from its types with @openpkg-ts/sdk;docs/api.mdis rendered from it
Regenerate against the latest Ashby spec with bun run generate --fetch, and the package's own spec + docs with bun run docs.
Notes
- Auth is Ashby's standard scheme: your API key as the Basic auth username, blank password. Keys and permissions are managed in Ashby's admin console.
- This client talks to your real ATS. Endpoints require the corresponding key permissions (
candidatesRead,candidatesWrite, ...), and write calls do what they say. - Not affiliated with Ashby. Ashby's API is versioned by them; this package tracks the published spec and vendored spec updates are explicit, reviewable diffs.
MIT
