Using your own website (portal API)
Power your existing careers site with Kepler’s open portal API — list jobs and take applications headlessly.
The hosted portal is the zero-effort option, but if you already run a careers site you can keep it and use Kepler as the engine behind it. The portal's public API is a stable, unauthenticated, CORS-open contract — your website can call it directly from the browser.
The endpoints
Your workspace's endpoints are listed (with copy buttons) on the Job board API tab of Settings → Developers. The two your developer needs most, the jobs feed and the apply form, are also in Settings → Job board under Put it on your website when you choose My developer builds our own job pages. The tab appears once your board has a URL, and also offers a ready-made Sample embed code snippet you can copy into your site:
| Endpoint | Purpose |
|---|---|
GET {portal URL}/jobs.json | Jobs feed, to import — each published job's full advert, an id to sync on, its industry (from a job field named Industry) and an apply_url. Jobs missing from the feed have closed. |
{portal URL}/jobs/{job-slug}/apply | Apply form, to iframe — the hosted apply form, to frame on your own job pages. It only loads on sites listed in Your website under Put it on your website in Settings → Job board. |
GET /portal/{slug} | Portal settings + the list of published postings (title, location, summary). |
GET /portal/{slug}/jobs/{job-slug} | The full advertised posting. |
GET /portal/{slug}/sitemap.xml | A sitemap of your published postings, for search engines. Empty when you choose My developer builds our own job pages, because Kepler's copies are then hidden from search. |
POST /portal/{slug}/jobs/{job-slug}/apply | Submit an application — multipart form data. |
The apply payload
The apply endpoint takes multipart form data with first_name, last_name and email (required), plus optional phone, location, linkedin_url and a resume file. It also expects a hidden website field that must be left empty — that's the spam trap.
If the job has screening questions, the job detail response lists them under screening_questions, each with an id, text, type, options and required. Send the answers as one screening_answers field: a JSON object keyed by question id. An application missing a required answer is rejected with an error for each question.
Consent fields for your own form
Read the board's consent object from the public response when building the form. It contains mode, text, privacy_url and text_version. The workspace controls these under Settings → Privacy & consent → Job board; see Consent & GDPR records.
| Mode | Show and submit |
|---|---|
off | No consent boxes or consent fields are needed. |
marketing_optional | Show an unticked optional “Keep me informed about similar roles” box. Send consent_marketing=yes when checked or no when unchecked. |
required | Show an unticked required storage box using text and the supplied privacy link, plus the optional marketing box. Send consent_storage=yes only after the applicant checks it, and consent_marketing=yes or no for their choice. |
For either enabled mode, include the supplied consent_text_version. Keep marketing optional and never preselect either box. Missing or unchecked storage consent in required mode rejects the application; disabling your submit button is helpful, but the server also validates it.
// Append to the same FormData as the applicant fields and optional resume.
if (consent.mode !== 'off') {
if (consent.mode === 'required') {
formData.append('consent_storage', storageChecked ? 'yes' : 'no');
}
formData.append('consent_marketing', marketingChecked ? 'yes' : 'no');
if (consent.text_version) {
formData.append('consent_text_version', consent.text_version);
}
}
Refresh the consent configuration when loading the form. If submission reports that storage consent is required, reload the current wording and ask the applicant to make their choice before retrying; do not automatically tick or submit consent on their behalf.
How it behaves
- No auth required — these endpoints are public by design and only ever expose advertised content. Internal fields are never present in responses.
- Open CORS — call them from any origin, including directly from client-side JavaScript.
- Same validation as the hosted portal — published-posting checks, email format validation, CV file type/size limits (PDF/DOC/DOCX, ≤ 10 MB), rate limiting, honeypot support and 24-hour dedupe.
- Same review flow — submissions create inbound applications and are parsed, scored and screened like hosted-portal applicants. Junk is moved to Screened out; everyone else waits for a recruiter on the Applicants page.
A minimal integration
- Fetch
GET /portal/{slug}and render the published postings as your job list. - Link each to a detail page fed by
GET /portal/{slug}/jobs/{job-slug}. - Build your apply form from the job's screening questions and the current consent configuration above, then
POSTmultipart form data to the apply endpoint, including the optional resume and applicable consent fields. - Include the
websitehoneypot field as a hidden input and leave it empty — submissions that fill it are rejected.
The hosted portal and this headless API use the same application-processing path, so applicants from both arrive on the same Applicants page.
If your site imports jobs.json into its own job pages, choose My developer builds our own job pages under Put it on your website in Settings → Job board and save, so Google credits your pages rather than Kepler's copies.
Postings you close in Kepler disappear from the list endpoint and stop accepting applications immediately. Refresh any cached job lists on your site, and show a clear message if a posting closes while someone is filling in the form.