[Intum Pomoc](https://intum.pl/pomoc.md) / [API](https://intum.pl/pomoc/rekrutacja/api.md)

# [Rekrutacja](https://intum.pl/pomoc/rekrutacja/api/rekrutacja.md)

Oferty pracy, grupy ofert, kandydaci, zgody RODO i ustawienia przez API modułu Rekrutacja.

**Autoryzacja:** `Authorization: Bearer TOKEN` - token musi mieć uprawnienie **recruit_jobs**
(kandydaci: **recruit_candidates**)
**Content-Type:** `application/json; charset=utf-8`

## API Endpoints

| Metoda | Ścieżka | Opis |
|--------|---------|------|
| GET | `/recruit/jobs.json` | Lista ofert pracy (filtry: `status`, `group_id`, `employment_type`, `work_mode`, `q`) |
| GET | `/recruit/jobs/:id.json` | Pojedyncza oferta (z `token` do publicznego ogłoszenia) |
| POST | `/recruit/jobs.json` | Utworzenie oferty |
| PATCH | `/recruit/jobs/:id.json` | Aktualizacja oferty |
| DELETE | `/recruit/jobs/:id.json` | Usunięcie oferty |
| GET | `/recruit/job_groups.json` | Lista grup ofert |
| GET | `/recruit/job_groups/:id.json` | Pojedyncza grupa (z `token` do publicznej listy grupy) |
| POST | `/recruit/job_groups.json` | Utworzenie grupy |
| PATCH | `/recruit/job_groups/:id.json` | Aktualizacja grupy |
| DELETE | `/recruit/job_groups/:id.json` | Usunięcie grupy (oferty zostają, tracą tylko `group_id`) |
| GET | `/recruit/candidates.json` | Lista kandydatów (filtry: `status`, `job_id`, `source`, `q`; domyślnie nowe zgłoszenia na górze) |
| GET | `/recruit/candidates/:id.json` | Pojedynczy kandydat |
| POST | `/recruit/candidates.json` | Dodanie kandydata |
| PATCH | `/recruit/candidates/:id.json` | Aktualizacja kandydata |
| PATCH | `/recruit/candidates/:id/change_status.json` | Zmiana etapu kandydata (`candidate[status]`) |
| POST | `/recruit/candidates/:id/merge.json` | Scalenie duplikatu: `source_id` = kandydat do wchłonięcia (znika, jego dane i pliki przechodzą do `:id`) |
| DELETE | `/recruit/candidates/:id.json` | Usunięcie kandydata (razem ze zgodami i plikami) |
| GET | `/recruit/settings.json` | Ustawienia modułu (`fields` z domyślnymi wartościami i ważnością zgód) |
| PATCH | `/recruit/settings.json` | Aktualizacja ustawień: `setting[notes]`, `setting[fields]` (JSON), własny baner stron publicznych multipartem (`setting[public_banner]`, usunięcie: `setting[public_banner_remove]=1`). `config` z tokenami jest tylko do odczytu |

## Pola oferty (`job`)

| Pole | Typ | Wymagane | Opis |
|------|-----|----------|------|
| `name` | string | tak | Stanowisko |
| `status` | string | nie | `draft` / `open` / `on_hold` / `closed` (domyślnie z ustawień, fabrycznie `open`) |
| `location` | string | nie | Lokalizacja |
| `employment_type` | string | nie | `permanent` / `b2b` / `permanent_or_b2b` (umowa o pracę lub B2B do wyboru) / `negotiable` (forma do ustalenia z kandydatem) / `mandate` / `temporary` / `internship` (domyślnie z ustawień) |
| `work_mode` | string | nie | `office` / `hybrid` / `remote` (domyślnie z ustawień) |
| `salary_from`, `salary_to` | decimal | nie | Widełki; `salary_to` nie może być mniejsze od `salary_from` |
| `currency` | string | nie | Kod waluty, zapisywany wielkimi literami; domyślnie z ustawień (fabrycznie `PLN`) |
| `positions_count` | integer | nie | Liczba etatów; puste = nie podano |
| `email`, `phone` | string | nie | Kontakt pokazywany na publicznym ogłoszeniu |
| `group_id` | integer lub string | nie | Grupa ofert: id istniejącej grupy **albo nazwa** - tekst zakłada nową grupę (lub bierze istniejącą o tej nazwie) przy zapisie oferty, np. `"group_id": "Magazyn Wrocław"` |
| `client_id` | integer | nie | Klient z CRM (rekrutacja dla klienta) - **nie wychodzi na publiczne ogłoszenie** |
| `responsible_id` | integer | nie | Prowadzący rekrutację (użytkownik) - wewnętrzne |
| `description` | text | nie | Opis stanowiska (widoczny w ogłoszeniu). Zwykły tekst, Markdown albo HTML - format rozpoznawany automatycznie przy renderowaniu (HTML gdy treść ma tagi, markdown gdy jednoznacznie na niego wygląda), treść jest sanityzowana |
| `fields` | object | nie | Dowolne pola dodatkowe (JSON) - wewnętrzne |

Tylko oferty ze statusem `open` wychodzą na publiczne listy i do widgetu.

## Pola grupy ofert (`job_group`)

| Pole | Typ | Wymagane | Opis |
|------|-----|----------|------|
| `name` | string | tak | Nazwa grupy („Magazyn Wrocław", „Rekrutacja jesień 2026") |
| `description` | text | nie | Opis wewnętrzny (nie pokazuje się na publicznej liście grupy) |
| `color` | string | nie | Kolor etykiety |

## Pola kandydata (`candidate`)

| Pole | Typ | Wymagane | Opis |
|------|-----|----------|------|
| `last_name` | string | tak | Nazwisko |
| `first_name`, `email`, `phone` | string | nie | Dane kontaktowe |
| `status` | string | nie | `unverified` / `screening` / `interview` / `hired` / `rejected` |
| `source` | string | nie | `direct` / `referral` / `job_board` / `linkedin` / `website` / `agency` / `other` |
| `job_id` | integer | nie | Rekrutacja, do której kandydat aplikuje |
| `position`, `location` | string | nie | Stanowisko i lokalizacja kandydata |
| `salary_expectation`, `currency` | decimal/string | nie | Oczekiwania finansowe |
| `available_from` | date | nie | Dostępność |
| `description` | text | nie | Notatka rekrutera |
| `fields` | object | nie | Dowolne pola dodatkowe (JSON); `fields.merged` to historia scaleń zapisywana przez system |

## Publiczne strony (bez logowania, bez tokenu Bearer)

| Ścieżka | Co pokazuje |
|---------|-------------|
| `/recruit/job/:token` | Ogłoszenie jednej oferty (`token` z GET `/recruit/jobs/:id.json`); `.json` daje publiczne pola oferty |
| `/recruit/job/:token/apply` | Formularz aplikacyjny (tylko dla oferty `open`, inaczej 403) |
| `/recruit/group/:token` | Otwarte oferty jednej grupy (`token` z GET `/recruit/job_groups/:id.json`) |
| `/recruit/offers/:token` | Wszystkie otwarte oferty konta; token generuje się przy pierwszym użyciu przycisku „Link publiczny ofert" na pulpicie (`config.public_jobs_token` w ustawieniach) |

Zgłoszenie z formularza: `POST /recruit/job/:token/apply` z polami `candidate[first_name]`,
`candidate[last_name]` (wymagane), `candidate[email]` (wymagane), `candidate[phone]`, `candidate[cv]`
(plik multipart: PDF/DOC/DOCX/ODT/RTF/TXT/obraz, do 10 MB), `candidate[description]` (wiadomość
kandydata „kilka słów o sobie", do 5000 znaków, zapisuje się jako notatka kandydata; wymagane
jest `cv` albo `description`) oraz zgodami `consent_recruitment=1`
(wymagana) i `consent_future=1` (dobrowolna). Publiczne listy pokazują maksymalnie 200 ofert;
formularz przyjmuje 5 zgłoszeń na minutę z jednego IP, strony publiczne 120 wejść na minutę.

Widget „aktualne oferty pracy” (kod do wklejenia jest na pulpicie modułu) czyta statyczny JSON
z CDN pod tokenem `config.widget_token`, odświeżany po każdej zmianie oferty - nie odpytuje API.

## Ustawienia modułu (`/recruit/settings`)

Pojedyncze wartości zmienia `POST /account/set_field` z `target: "recruit_setting"`:

| `key` | Wartości |
|-------|----------|
| `recruit.jobs.default_status` | `draft` / `open` / `on_hold` / `closed` |
| `recruit.jobs.default_employment_type` | jak `employment_type` oferty |
| `recruit.jobs.default_work_mode` | jak `work_mode` oferty |
| `recruit.jobs.default_currency` | kod waluty, np. `PLN` |
| `recruit.consents.months_recruitment` | ile miesięcy ważna jest zgoda na tę rekrutację (fabrycznie 12) |
| `recruit.consents.months_future` | ile miesięcy ważna jest zgoda na przyszłe rekrutacje (fabrycznie 6) |

Przykład: `POST /account/set_field` `{"key":"recruit.jobs.default_currency","value":"EUR","target":"recruit_setting"}`.
Te same wartości widać w `fields` z GET `/recruit/settings.json`.

Własny baner stron publicznych (obrazek nad ogłoszeniem i listami ofert zamiast banera z logo produktu;
formularz zgłoszeniowy banera nie ma) to plik, nie wartość w `fields` - wgrywa się go
`PATCH /recruit/settings.json` jako `multipart/form-data` z polem `setting[public_banner]`
(PNG, JPG, WEBP lub GIF, do 5 MB; najlepiej ok. 1536 × 288 px, brzegi mogą zostać przycięte na telefonie).
`setting[public_banner_remove]=1` usuwa go i przywraca baner z logo. Adres wgranego banera zwraca
GET `/recruit/settings.json` w `public_banner_url` (`null`, gdy nie wgrano).

## Zasady

- oferta bez `status` dostaje domyślny z ustawień modułu; `work_mode`, `employment_type`
  i `currency` działają tak samo
- kandydat z publicznego formularza ma `source: "direct"`, etap `unverified` i puste `seen_at`
  (na liście pokazuje się jako „Nowy"); wejście na jego kartę ustawia `seen_at`
- zgody RODO (`Recruit::Consent`) powstają tylko przez publiczny formularz - API ich nie tworzy;
  zapis zgłoszenia jest transakcją (bez zapisanej zgody nie ma kandydata)
- wygasła zgoda jest oznaczana na karcie kandydata; automatycznej anonimizacji jeszcze nie ma
- publiczne strony pokazują tylko dane ogłoszenia: nazwę, lokalizację, formę zatrudnienia, tryb
  pracy, widełki, opis i kontakt - `client_id`, `responsible_id`, `fields` i opis grupy zostają wewnętrzne