Public Jobs API
Build your own job site (e.g. with Next.js on Vercel) on top of your Kit hiring pipeline. List published roles and receive applications through a public REST API, an official TypeScript SDK, and a one-click Next.js template.
Why It Matters
Kit’s hosted career portal and embeddable widget cover most needs. But if you want full control over design — a bespoke careers site, a custom landing page per role, or a job board that matches your product — the Public Jobs API lets you read your published jobs and submit applications straight into your Kit pipeline, while Kit keeps owning screening, stages, interviews, and candidate communication.
There’s also an official TypeScript SDK and a one-click Next.js template so you can ship a custom job site in minutes — or skip both and call the REST endpoints below directly with any HTTP client.
API Keys
Create a key pair under Hiring → Career Portal → Public API Keys. Each pair has:
-
Publishable key (
pk_…) — safe to ship in a browser. It can read published jobs and submit applications, nothing else, and it never exposes candidate data. Before it can submit applications, you must configure at least one bot defence — an origin allowlist or your own Cloudflare Turnstile widget — otherwise application requests are rejected with403 bot_protection_required. Reading jobs works without it. -
Secret key (
sk_…) — for server-side use only (e.g. a Next.js Server Action). It skips the browser origin/Turnstile checks. Never expose it in client-side code. Neither key can read candidate PII.
The secret key is shown only once, when created or rotated. Rotate it any time from the key’s settings page; the previous secret stops working immediately.
Authenticate every request with a bearer header:
Authorization: Bearer sk_your_secret_key
Endpoints
Base URL: https://startupkit.app (or your career custom domain).
List published jobs
GET /api/public/v1/jobs?department=&location=&employment_type=&remote=&page=&per_page=
Returns only published roles for your account.
{
"data": [
{
"id": "JdK2hQ8…",
"title": "Senior Rails Developer",
"department": "Engineering",
"location": "Remote",
"employment_type": "full_time",
"remote": true,
"published_at": "2026-06-01T12:00:00Z",
"url": "https://careers.yourco.com/JdK2hQ8…",
"salary": { "min": 120000, "max": 160000, "currency": "USD", "period": "YEAR" }
}
],
"pagination": { "current_page": 1, "total_pages": 3, "total_count": 42, "per_page": 20 }
}
The id is the job’s public token — use it for the detail and apply endpoints.
Get a job + its application form
GET /api/public/v1/jobs/:public_token
Returns the job plus an application_form describing exactly which fields and questions to render, the consent disclosure to show, accepted resume types/size, and whether Turnstile is required.
{
"id": "JdK2hQ8…",
"title": "Senior Rails Developer",
"description_html": "<p>We're hiring…</p>",
"accepting_applications": true,
"stages": [{ "name": "Application Review", "type": "application_form" }],
"application_form": {
"fields": [
{ "name": "cover_letter", "type": "textarea", "label": "Cover letter", "required": false }
],
"questions": [
{ "key": "why_us", "type": "text", "prompt": "Why do you want to join?", "required": true, "max_length": 2000 }
],
"consent_disclosure_html": "<p>By applying you agree…</p>",
"resume": {
"content_types": ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
"max_byte_size": 10485760
},
"turnstile": { "required": false, "sitekey": null }
}
}
Upload a resume (presigned)
Resumes upload directly to storage, so they never pass through your server (avoiding serverless body-size limits).
POST /api/public/v1/direct_uploads
{ "blob": { "filename": "cv.pdf", "byte_size": 102400, "checksum": "<base64 MD5>", "content_type": "application/pdf" } }
{
"signed_id": "eyJf…",
"direct_upload": { "url": "https://…s3…", "headers": { "Content-Type": "application/pdf", "Content-MD5": "…" } }
}
PUT the file bytes to direct_upload.url with the returned headers, then pass the signed_id as resume_signed_id when you submit the application.
Submit an application
POST /api/public/v1/jobs/:public_token/applications
{
"application": {
"email": "[email protected]",
"first_name": "Ada",
"last_name": "Lovelace",
"phone": "+1 555 0100",
"responses": { "cover_letter": "…", "why_us": "…" },
"resume_signed_id": "eyJf…"
},
"turnstile_token": "<token>"
}
Returns 201 with a minimal, PII-free confirmation:
{ "id": "app_9fQ…", "status": "submitted", "job": "JdK2hQ8…", "submitted_at": "2026-06-11T09:30:00Z" }
turnstile_token is only needed for browser (pk_) submissions when the key has Turnstile configured; server-side (sk_) calls skip it.
Errors
Errors return a consistent envelope:
{ "error": { "code": "validation_failed", "message": "Email can't be blank", "fields": { "email": ["can't be blank"] } } }
| Status | Code | Meaning |
|---|---|---|
| 401 | invalid_key |
Missing or invalid API key |
| 403 | bot_protection_required |
Publishable (pk_) key has no origin allowlist or Turnstile configured |
| 403 | origin_not_allowed |
Browser origin not in the key’s allowlist |
| 404 | not_found |
Job not found or not published |
| 409 | already_applied |
This email already applied to this job |
| 422 | validation_failed |
Invalid application fields (see fields) |
| 422 | turnstile_failed |
Turnstile verification failed |
| 422 |
invalid_content_type / file_too_large
|
Rejected resume upload |
Status Updates via Webhooks
To track applications after submission, configure outbound webhooks. Relevant events include application.submitted, application.advanced, and application.rejected, plus job_posting.published/paused/closed. Application payloads include both the numeric id and the API prefix_id (app_…), and the job’s public_token, so you can correlate webhook events with API records.
SDK & Next.js Template
Two official, open-source starting points sit on top of the REST contract above — use either, or skip both and call the endpoints directly with any HTTP client.
-
TypeScript SDK —
@startupkit-app/jobs. A typed, zero-dependency client (nativefetch, ESM + CJS, Node ≥ 18.17) that runs in Node, browsers, and edge runtimes. Install withnpm install @startupkit-app/jobs, then:import { createClient } from "@startupkit-app/jobs"; const kit = createClient({ secretKey: process.env.KIT_SECRET_KEY }); const page = await kit.listJobs({ department: "Engineering", remote: true }); const job = await kit.getJob(page.data[0].id); const { signed_id } = await kit.uploadFile(resumeFile); await kit.apply(job.id, { email: "[email protected]", resume_signed_id: signed_id });Pass
publishableKey(pk_…) instead ofsecretKeyin browser code. Other methods:allJobs()async-iterates every page, andcreateUpload()gives lower-level control over the presigned upload. Non-2xx responses throwKitApiError(with.codeand.fields); failures that never reach the API throwKitNetworkError. The client defaults to thehttps://app.startupkit.appbase URL; passbaseUrlto override it for a custom career domain. -
Next.js template —
nextjs-job-board. A production-ready careers site (Next.js App Router, Server Components + Server Actions, ISR with tag-based revalidation) you can fork or one-click deploy. It renders the application form dynamically from the API schema, does direct-to-storage presigned resume uploads, emits schema.orgJobPostingJSON-LD (Google for Jobs ready), and optionally revalidates instantly via webhooks. Set one environment variable —STARTUPKIT_SECRET_KEY(yoursk_…key) — and deploy:Live demo: nextjs-job-board-orcin.vercel.app. Optional environment variables:
STARTUPKIT_BASE_URL(defaults tohttps://app.startupkit.app),NEXT_PUBLIC_TURNSTILE_SITE_KEY,REVALIDATE_SECRET, andNEXT_PUBLIC_COMPANY_NAME.
Both consume the contract above, so you can also build against any framework using plain HTTP. A typical custom job site wires up four calls: list jobs, fetch a job with its form, request a presigned upload for the resume, and submit the application. Because these are plain JSON over HTTPS, the same integration works from a server (sk_ key) or the browser (pk_ key with a bot defence configured).
Rate Limits
Application submissions are limited to 10/hour per IP and upload requests to 30/hour per IP, alongside the global API rate limits. Browser keys are additionally protected by their origin allowlist and optional Turnstile.