@startupkit-app/jobs
v0.1.1
Published
Typed, zero-dependency fetch client for Kit's (startupkit.app) public hiring API — list published jobs and submit applications from your own job site.
Maintainers
Readme
@startupkit-app/jobs
Typed, zero-dependency JavaScript/TypeScript client for Kit's public hiring API. List your published jobs and accept applications from your own careers site — Node, browsers, edge runtimes, any framework.
- Zero runtime dependencies — native
fetchonly - ESM + CJS, fully typed, tree-shakeable
- snake_case wire format preserved — what the API returns is what you get
- Node
>= 18.17
Install
npm install @startupkit-app/jobsKeys
Create API keys in Kit under Hiring → Settings → API. There are two kinds:
| Key | Prefix | Where | CORS |
| --- | --- | --- | --- |
| Publishable | pk_… | Browsers | Only from Origins you allowlist |
| Secret | sk_… | Servers only | No CORS — never ship to a browser |
Pass exactly one to createClient. The SDK throws if you pass both, and throws if it detects a secretKey being used in a browser.
Quickstart (server-side, sk_…)
import { createClient } from "@startupkit-app/jobs";
const kit = createClient({ secretKey: process.env.KIT_SECRET_KEY });
// One page at a time
const page = await kit.listJobs({ department: "Engineering", remote: true });
for (const job of page.data) {
console.log(job.title, job.location, job.url);
}
if (page.hasNextPage) {
const next = await page.nextPage()!;
}
// Or iterate everything
for await (const job of kit.allJobs()) {
console.log(job.id, job.title);
}
// Full detail: description, stages, and the application form definition
const job = await kit.getJob(page.data[0].id);
console.log(job.description_html, job.accepting_applications);Quickstart (browser, pk_… + Turnstile)
Publishable keys are safe to embed in frontend code. If the job's
application_form.turnstile.required is true, render the
Cloudflare Turnstile widget
with application_form.turnstile.sitekey and pass the resulting token
through to apply:
import { createClient } from "@startupkit-app/jobs";
const kit = createClient({ publishableKey: "pk_live_…" });
const job = await kit.getJob("tok_abc123");
// Render job.application_form.fields and job.application_form.questions,
// show job.application_form.consent_disclosure_html near the submit button.
const result = await kit.apply(
job.id,
{
email: "[email protected]",
first_name: "Jane",
last_name: "Doe",
responses: { motivation: "…" }, // keyed by Question.key
},
{ turnstileToken } // from the Turnstile widget callback
);
console.log(result.id, result.status); // "app_…", "submitted"Resume upload flow
uploadFile does the whole direct-upload dance for you: it computes the
base64 MD5 checksum, registers the blob, PUTs the bytes straight to
storage, and hands back the signed_id to attach to the application.
const file = fileInput.files[0]; // or a Blob in Node
// Optional client-side validation against the job's constraints:
const { content_types, max_byte_size } = job.application_form.resume;
const { signed_id } = await kit.uploadFile(file);
await kit.apply(job.id, {
email: "[email protected]",
resume_signed_id: signed_id,
});Need lower-level control? createUpload(meta) registers the blob and
returns the direct_upload.url + direct_upload.headers so you can run
the PUT yourself.
Error handling
Non-2xx API responses throw KitApiError; failures that never reach the
API (network, DNS, the direct-upload PUT) throw KitNetworkError.
import { KitApiError, KitNetworkError } from "@startupkit-app/jobs";
try {
await kit.apply(job.id, { email });
} catch (error) {
if (error instanceof KitApiError) {
switch (error.code) {
case "already_applied": // 409
break;
case "validation_failed": // 422 — see error.fields
console.log(error.fields); // { email: ["is invalid"], … }
break;
case "turnstile_failed": // 422 — refresh the widget and retry
break;
default:
console.error(error.status, error.code, error.message);
}
} else if (error instanceof KitNetworkError) {
// retry / offline UI
}
}Error codes: invalid_key (401), origin_not_allowed (403), not_found (404),
already_applied (409), validation_failed / turnstile_failed /
invalid_content_type / file_too_large (422), parameter_missing (400).
API
createClient({ publishableKey?, secretKey?, baseUrl? }): KitJobsClient
client.listJobs(params?): Promise<Page<Job>>
client.allJobs(params?): AsyncIterable<Job>
client.getJob(publicToken): Promise<JobDetail>
client.createUpload(meta): Promise<UploadTicket>
client.uploadFile(file, meta?): Promise<{ signed_id: string }>
client.apply(publicToken, input, opts?): Promise<ApplicationResult>All request/response types (Job, JobDetail, ApplicationInput,
ApplicationResult, Page, FormField, Question, …) are exported.
Examples
examples/node-list-jobs.ts— server-side listingexamples/browser-apply.html— browser form with Turnstile + resume upload
Releasing (maintainers)
Releases publish to npm via GitHub Actions + npm Trusted Publishing (OIDC) —
no NPM_TOKEN secret, provenance attached automatically.
- Bump the version (
npm version patch|minor|major) and merge tomain. - The
Releaseworkflow publishes the new version via OIDC.
The workflow is idempotent — it only publishes when package.json's version
isn't already on the registry, and is gated behind the repo variable
NPM_PUBLISH_ENABLED. The initial 0.1.0 was published locally (npm requires a
package to exist before a Trusted Publisher can be configured); every release
after is CI-only.
License
MIT
