Publiczne API ofert pracy
Zbuduj własną stronę z ofertami pracy (np. w Next.js na Vercel) na bazie pipeline'u rekrutacyjnego Kit. Pobieraj opublikowane ogłoszenia i przyjmuj aplikacje przez publiczne REST API, oficjalne SDK w TypeScript oraz szablon Next.js wdrażany jednym kliknięciem.
Dlaczego to ważne
Hostowany portal kariery i widget do osadzania zaspokajają większość potrzeb. Jeśli jednak chcesz mieć pełną kontrolę nad designem (własną stronę kariery, osobny landing page dla każdego stanowiska albo portal z ofertami dopasowany do twojego produktu), Publiczne API ofert pracy pozwala odczytywać opublikowane ogłoszenia i przesyłać aplikacje prosto do pipeline’u w Kit, podczas gdy Kit nadal odpowiada za screening, etapy, rozmowy i komunikację z kandydatami.
Dostępne są też oficjalne SDK w TypeScript oraz szablon Next.js wdrażany jednym kliknięciem, dzięki którym uruchomisz własną stronę z ofertami w kilka minut. Albo pomiń oba i wywołuj opisane niżej endpointy REST bezpośrednio, dowolnym klientem HTTP.
Klucze API
Utwórz parę kluczy w Hiring → Career Portal → Public API Keys. Każda para składa się z:
-
Klucz publikowalny (
pk_…) — bezpieczny do użycia w przeglądarce. Może odczytywać opublikowane ogłoszenia i przesyłać aplikacje, nic więcej, i nigdy nie ujawnia danych kandydatów. Zanim będzie mógł przesyłać aplikacje albo prosić o presigned upload, musisz skonfigurować przynajmniej jedno zabezpieczenie przed botami (listę dozwolonych originów albo własny widget Cloudflare Turnstile), w przeciwnym razie takie żądania są odrzucane z403 bot_protection_required. Odczyt ogłoszeń działa bez tej konfiguracji. -
Tajny klucz (
sk_…) — wyłącznie do użycia po stronie serwera (np. w Server Action w Next.js). Pomija przeglądarkowe sprawdzanie originu i Turnstile. Nigdy nie umieszczaj go w kodzie po stronie klienta. Żaden z kluczy nie ma dostępu do danych osobowych kandydatów.
Tajny klucz jest pokazywany tylko raz, przy utworzeniu lub rotacji. Możesz go zrotować w dowolnym momencie ze strony ustawień klucza; poprzedni klucz natychmiast przestaje działać.
Każde żądanie uwierzytelniaj nagłówkiem bearer:
Authorization: Bearer sk_your_secret_key
Endpointy
Bazowy URL: https://startupkit.app (albo twoja niestandardowa domena kariery).
Pobieranie listy opublikowanych ogłoszeń
GET /api/public/v1/jobs?department=&location=&employment_type=&remote=&page=&per_page=
Zwraca wyłącznie opublikowane ogłoszenia z twojego konta.
{
"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 }
}
id to publiczny token ogłoszenia; użyj go w endpointach szczegółów i aplikowania.
Pobieranie ogłoszenia z formularzem aplikacyjnym
GET /api/public/v1/jobs/:public_token
Zwraca ogłoszenie wraz z application_form, który dokładnie opisuje, jakie pola i pytania wyrenderować, jaką klauzulę zgody pokazać, czy CV jest wymagane i jakie jego typy oraz rozmiary plików są akceptowane, a także czy wymagany jest Turnstile.
{
"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": {
"required": false,
"content_types": ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
"max_byte_size": 10485760
},
"turnstile": { "required": false, "sitekey": null }
}
}
Flagi required są egzekwowane po stronie serwera. Gdy resume.required ma wartość true, aplikacja bez resume_signed_id zostaje odrzucona z 422 validation_failed, tak samo jak przy każdym pominiętym polu lub pytaniu oznaczonym required: true. Jeśli renderujesz formularz dynamicznie na podstawie tego schematu, uwzględnij te flagi, żeby kandydat nie zobaczył odmowy dopiero po wypełnieniu całego formularza.
Przesyłanie CV (presigned upload)
CV trafiają bezpośrednio do magazynu plików, więc nigdy nie przechodzą przez twój serwer (co omija limity rozmiaru body w środowiskach serverless).
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": "…" } }
}
Wyślij bajty pliku metodą PUT na direct_upload.url ze zwróconymi headers, a następnie przekaż signed_id jako resume_signed_id przy wysyłaniu aplikacji.
Wysyłanie aplikacji
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>"
}
Zwraca 201 z minimalnym potwierdzeniem bez danych osobowych:
{ "id": "app_9fQ…", "status": "submitted", "job": "JdK2hQ8…", "submitted_at": "2026-06-11T09:30:00Z" }
turnstile_token jest potrzebny tylko przy wysyłce z przeglądarki (pk_), gdy klucz ma skonfigurowany Turnstile; wywołania po stronie serwera (sk_) go pomijają.
Pula talentów
Nie każdy, komu podoba się twoja firma, pasuje dziś do któregoś z otwartych stanowisk. Pula talentów zbiera właśnie takie osoby (adres e-mail, LinkedIn, opcjonalnie CV), żeby można było odezwać się do nich, gdy otworzy się odpowiednia rekrutacja. Wpisy trafiają tam jako niezweryfikowane: Kit wysyła mailem link potwierdzający i nikt nie znajdzie się w puli przez ten endpoint, dopóki w niego nie kliknie. (Administratorzy mogą też importować CV bezpośrednio — to osobne wejście, które rządzi się własnymi zasadami.)
Zapis do puli talentów to prośba o przechowywanie danych, przy której nie ma żadnej rekrutacji, na jaką można by zaaplikować. Dlatego wymaga wyraźnej zgody: faktycznie zaznaczonego pola wyboru, a nie samej klauzuli pokazanej na stronie. Publiczny zapis bez niej zostaje odrzucony. Import wykonany przez administratora zapisuje zamiast tego oświadczenie o podstawie prawnej — tam nie ma nikogo, kto mógłby cokolwiek zaznaczyć.
Pobieranie formularza zapisu
GET /api/public/v1/talent_pool
{
"accepting_signups": true,
"consent": {
"required": true,
"disclosure_html": "<p>Keep my details on file for 24 months…</p>",
"retention_months": 24,
"privacy_policy_url": "https://yourco.com/privacy"
},
"fields": [
{ "name": "email", "required": true },
{ "name": "linkedin_url", "required": false },
{ "name": "resume_signed_id", "required": false }
],
"resume": {
"required": false,
"content_types": ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
"max_byte_size": 10485760
},
"turnstile": { "required": false, "sitekey": null }
}
Wyrenderuj consent.disclosure_html jako etykietę pola wyboru, które startuje niezaznaczone. To twoja własna treść zgody, edytowana w Hiring → Settings → Consent razem z retention_months, które decyduje o tym, kiedy Kit anonimizuje wpisy, których nikt nie odnowił.
Zapis do puli talentów
POST /api/public/v1/talent_pool/entries
{
"talent_pool_entry": {
"email": "[email protected]",
"linkedin_url": "https://linkedin.com/in/ada",
"resume_signed_id": "eyJf…",
"consent": true,
"consent_ip_address": "203.0.113.7"
},
"turnstile_token": "<token>"
}
Zwraca 201 z potwierdzeniem bez danych osobowych:
{ "id": "tpe_9fQ…", "status": "pending_verification", "submitted_at": "2026-06-11T09:30:00Z" }
CV przesyła się dokładnie tak samo jak przy aplikacjach: poproś o signed_id, wyślij bajty metodą PUT, a potem przekaż go jako resume_signed_id.
Rejestrowanie tego, kto wyraził zgodę
Przy każdym wpisie Kit przechowuje poświadczenie zgody: dokładną treść, którą pokazano, znacznik czasu oraz adres IP osoby, która zgodę wyraziła. To ostatnie pole jest tym, na którym integracje po stronie serwera niezauważenie się mylą.
Przy wysyłce z przeglądarki kluczem pk_ Kit sam obserwuje adres IP i ignoruje consent_ip_address, które przyślesz. Klucz trafiający do JavaScriptu na stronie nie może sam wypisywać sobie poświadczenia.
Przy wysyłce z twojego serwera kluczem sk_ Kit widzi adres twojego serwera, a nie osoby, która się zapisuje. Prawdziwy adres prześlij w consent_ip_address; w Server Action na Vercelu to pierwszy adres z nagłówka x-forwarded-for:
import { headers } from "next/headers";
const forwarded = (await headers()).get("x-forwarded-for");
const consentIp = forwarded?.split(",")[0]?.trim();
Tę wartość oświadczasz ty, a Kit jej nie dowodzi: zapisuje to, co przyślesz, o ile da się to zinterpretować jako adres IP, a wszystko inne odrzuca z 422 invalid_consent_ip. Jeśli naprawdę go nie znasz, pomiń to pole. Kit nie zapisze wtedy żadnego adresu, a uczciwa luka jest lepsza niż poświadczenie wskazujące niewłaściwą maszynę.
Błędy
Błędy zwracane są w spójnej kopercie:
{ "error": { "code": "validation_failed", "message": "Email can't be blank", "fields": { "email": ["can't be blank"] } } }
| Status | Kod | Znaczenie |
|---|---|---|
| 401 | invalid_key |
Brakujący lub nieprawidłowy klucz API |
| 403 | bot_protection_required |
Klucz publikowalny (pk_) nie ma skonfigurowanej listy dozwolonych originów ani Turnstile |
| 403 | origin_not_allowed |
Origin przeglądarki spoza listy dozwolonych dla klucza |
| 404 | not_found |
Ogłoszenie nie istnieje lub nie jest opublikowane |
| 409 | already_applied |
Z tego adresu e-mail już zaaplikowano na to ogłoszenie |
| 409 | already_in_talent_pool |
Ten adres e-mail jest już w puli talentów |
| 422 | validation_failed |
Nieprawidłowe pola aplikacji, w tym brak wymaganego CV albo pominięte wymagane pole lub pytanie (zobacz fields) |
| 422 | consent_required |
Zapis do puli talentów przyszedł bez zaznaczonego pola zgody |
| 422 | invalid_consent_ip |
consent_ip_address nie jest prawidłowym adresem IP |
| 422 | turnstile_failed |
Weryfikacja Turnstile nie powiodła się |
| 422 |
invalid_content_type / file_too_large / invalid_byte_size
|
Odrzucony upload CV |
Aktualizacje statusu przez webhooki
Żeby śledzić aplikacje po ich wysłaniu, skonfiguruj webhooki wychodzące. Istotne zdarzenia to m.in. application.submitted, application.advanced i application.rejected, a także job_posting.published/paused/closed. Payloady aplikacji zawierają zarówno numeryczne id, jak i prefix_id z API (app_…) oraz public_token ogłoszenia, więc zdarzenia z webhooków łatwo powiążesz z rekordami z API.
SDK i szablon Next.js
Dwa oficjalne punkty wyjścia o otwartym kodzie opierają się na opisanym wyżej kontrakcie REST. Użyj jednego z nich albo pomiń oba i wywołuj endpointy bezpośrednio dowolnym klientem HTTP.
-
SDK w TypeScript —
@startupkit-app/jobs. Typowany klient bez zależności (natywnyfetch, ESM + CJS, Node ≥ 18.17), działający w Node, przeglądarkach i runtime’ach edge. Zainstaluj go poleceniemnpm install @startupkit-app/jobs, a następnie: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 });W kodzie przeglądarkowym przekaż
publishableKey(pk_…) zamiastsecretKey. Pozostałe metody:allJobs()iteruje asynchronicznie po wszystkich stronach,createUpload()daje kontrolę nad presigned uploadem na niższym poziomie, agetTalentPool()/joinTalentPool()obsługują opisany wyżej zapis do puli talentów. Odpowiedzi inne niż 2xx rzucająKitApiError(z.codei.fields); błędy, które nigdy nie docierają do API, rzucająKitNetworkError. Klient domyślnie używa bazowego URLhttps://app.startupkit.app; przekażbaseUrl, aby nadpisać go własną domeną kariery. -
Szablon Next.js —
nextjs-job-board. Gotowa do produkcji strona kariery (Next.js App Router, Server Components + Server Actions, ISR z rewalidacją opartą na tagach), którą możesz sforkować albo wdrożyć jednym kliknięciem. Renderuje formularz aplikacyjny dynamicznie ze schematu API, wykonuje presigned uploady CV bezpośrednio do magazynu, emituje JSON-LD schema.orgJobPosting(gotowe pod Google for Jobs) i opcjonalnie rewaliduje natychmiast przez webhooki. Ustaw jedną zmienną środowiskowąSTARTUPKIT_SECRET_KEY(twój kluczsk_…) i wdróż:Demo na żywo: nextjs-job-board-orcin.vercel.app. Opcjonalne zmienne środowiskowe:
STARTUPKIT_BASE_URL(domyślniehttps://app.startupkit.app),NEXT_PUBLIC_TURNSTILE_SITE_KEY,REVALIDATE_SECRETorazNEXT_PUBLIC_COMPANY_NAME.
Oba korzystają z opisanego wyżej kontraktu, więc możesz też budować w dowolnym frameworku przez zwykłe HTTP. Typowa własna strona z ofertami łączy cztery wywołania: pobranie listy ogłoszeń, pobranie ogłoszenia z formularzem, poproszenie o presigned upload dla CV oraz wysłanie aplikacji. Ponieważ wszystko to zwykły JSON po HTTPS, ta sama integracja działa po stronie serwera (klucz sk_) i w przeglądarce (klucz pk_ ze skonfigurowanym zabezpieczeniem przed botami).
Limity zapytań
Wysyłanie aplikacji jest ograniczone do 10/godz. na IP, a żądania uploadu do 30/godz. na IP i 300/godz. na klucz, oprócz globalnych limitów API. Klucze przeglądarkowe są dodatkowo chronione listą dozwolonych originów i opcjonalnie przez Turnstile.
Zapisy do puli talentów są ograniczone do 100/godz. na klucz. Zapisy z przeglądarki (pk_) mają dodatkowo limit 5/godz. na IP, a serwerowe (sk_) nie mają go wcale: wszystkie docierają do Kit z tego samego adresu wyjściowego, więc limit na IP dławiłby całą twoją stronę zamiast jednej osoby.