# Using your own website (portal API)

> Power your existing careers site with Kepler’s open portal API — list jobs and take applications headlessly.

_Collection: Job Advertising & Portal (job-portal) — Kepler Help Center. Canonical: https://keplercrm.com/support/articles/portal-api-your-own-website/_

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](/support/articles/consent-and-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](/support/articles/review-inbound-applicants/).

## 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 `POST` multipart form data to the apply endpoint, including the optional resume and applicable consent fields.

  - Include the `website` honeypot 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.

> **NOTE:** 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.
