Element External Integration API — dokumentacja

v1-beta dokument v1.3 · 2026-07-15 Beta

REST API do integracji zewnętrznych systemów (voice-boty, ATS, pipeline'y danych) z platformą Element. Interfejs jest w fazie beta — kontrakt może się jeszcze zmienić w niekompatybilny sposób w oknie ~2–3 miesięcy testów produkcyjnych; zmiany łamiące będą komunikowane z wyprzedzeniem.

◆ Zmiany w wersji 1.3 (2026-07-15)
  • POST …/apply — pole email opcjonalne. E-mail nie jest już wymagany przy składaniu aplikacji; jeśli go podasz, nadal jest walidowany (błędny format → 400 validation_failed). Aplikacja bez e-maila tworzy kandydata bez adresu. Zob. 8.11.
  • POST …/apply — dane podstawowe w podglądzie formularza. Imię, nazwisko, e-mail i telefon przekazane w aplikacji przez API trafiają teraz do arkusza odpowiedzi i są widoczne w podglądzie formularza kandydata w Element. Bez zmian w kontrakcie żądania i odpowiedzi; dotyczy aplikacji złożonych po wdrożeniu. Zob. 8.11.

Zmiany z wersji 1.2 (rozwinięte subForms[], załączniki attachments[], odpowiedzi calendarDay) pozostają oznaczone znaczkami „v1.2" w sekcjach 8.6 i 8.11.


1O API

External Integration API udostępnia programowy dostęp tylko-do-odczytu do projektów rekrutacyjnych, kandydatów i formularzy aplikacyjnych oraz ograniczony zestaw operacji zapisu (utworzenie kandydata przez aplikację, dodanie publicznej notatki). Adresowany jest do zespołów developerskich wdrażających integracje z systemami zewnętrznymi.

Wszystkie ścieżki są wersjonowane prefiksem /api/v1-beta. Po stabilizacji interfejs przejdzie w zamrożone v1; przyszłe zmiany łamiące trafią pod nowy prefiks (np. /api/v2).

2Wymagania wstępne

  • Aktywny tenant Element z włączonym modułem External Integration API.
  • Konto API (login + hasło) utworzone w panelu — jeden zestaw poświadczeń na jeden system zewnętrzny.
  • Klient HTTP obsługujący uwierzytelnianie HTTP Basic oraz multipart/form-data (dla przesyłania plików).

3Pierwsze wywołanie

Najprostszy test łączności — endpoint health (bez uwierzytelniania):

curl https://<tenant>.elementapp.ai/api/v1-beta/health

# 200 OK
{ "status": "ok", "apiVersion": "v1-beta" }

Kolejny krok — uwierzytelnione pobranie listy projektów (zob. 8.2).

4Uwierzytelnianie

Schemat: HTTP Basic (RFC 7617), realm External Integration API.

  • login — w formacie api+<nazwa-systemu>@<tenant>.local (np. api+voicebot@acme.local).
  • hasło — generowane losowo, pokazywane jednorazowo przy utworzeniu, nieodzyskiwalne.
  • nagłówekAuthorization: Basic base64(login:hasło).

Wielotenantowość: tenant identyfikowany jest wyłącznie po subdomenie w URL (<tenant>.elementapp.ai); poświadczenia są zablokowane do jednego tenanta.

⚠ Ochrona przed brute-force
  • (IP + login): 5 nieudanych prób → blokada 15 min.
  • sam login: 20 nieudanych prób → blokada 15 min.
  • Odpowiedź: 429 z kodem too_many_failed_attempts.

5Format danych

  • Odpowiedziapplication/json; charset=utf-8.
  • Żądaniaapplication/x-www-form-urlencoded lub multipart/form-data (dla plików).
  • Daty — ISO 8601 UTC z offsetem (2026-05-28T12:34:56+00:00); daty-tylko jako YYYY-MM-DD.
  • Identyfikatory — UUID v4 (np. ff315ea1-c056-472d-aefe-b99f3d41e5c0).
  • Lokalizacja — parametr locale (pl, en, de, …) dla schematu formularza i odpowiedzi.

Koperta odpowiedzi (sukces):

{ "data": { /* encja lub tablica */ },
  "meta": { "page": 1, "perPage": 20, "total": 8 } }

Koperta odpowiedzi (błąd):

{ "error": { "code": "snake_case_identifier",
             "message": "Czytelny opis (dla wybranych kodów)" } }

Paginacja: ?page=N&perPage=N (1–100, domyślnie 20); w odpowiedzi meta: { page, perPage, total }.

6Limity zapytań

  • Domyślnie: 70 wywołań/min na poświadczenie (60 bazowe + 10 burst) ≈ 1,2 req/s.
  • Odpowiedź przy przekroczeniu: 429 z kodem rate_limit_exceeded.
  • Backoff: wykładniczy od 1 s, limit 30 s.
  • Wyższe limity — kontakt ze wsparciem Element.

7Obsługa błędów

HTTPcodeZnaczenie
200Sukces (GET)
201Utworzono (POST)
400validation_failedNieprawidłowe/niekompletne żądanie
400invalid_answersPOST /apply: błędny JSON lub kształt/wartości answers
401Brak / błędne uwierzytelnianie
403Brak roli lub konto dezaktywowane
404project_not_foundProjekt nie istnieje / zarchiwizowany / spoza tenanta
404candidate_not_foundKandydat nie istnieje / zarchiwizowany / usunięty RODO
404application_form_not_foundBrak formularza / niewypełniony przez kandydata
409no_application_stageProjekt bez etapu aplikacyjnego
409stale_form_versionPOST /apply: niezgodna wersja formularza
429rate_limit_exceededPrzekroczony limit zapytań
429too_many_failed_attemptsBlokada po nieudanych logowaniach
5xxBłąd serwera Element; ponów z backoffem

8Referencja endpointów

Base URL: https://<tenant>.elementapp.ai/api/v1-beta

8.1 — Health check

GET/health

Kontrola łączności bez uwierzytelniania.

{ "status": "ok", "apiVersion": "v1-beta" }

8.2 — Lista projektów

GET/projects?page={n}&perPage={n}

Stronicowana lista aktywnych projektów.

{ "data": [
    { "id": "ff315ea1-…", "name": "Senior PHP Developer",
      "positionName": null, "isRemote": true,
      "statusId": "73f09ef2-…", "createdAt": "2021-10-31T11:42:24+00:00" } ],
  "meta": { "page": 1, "perPage": 20, "total": 8 } }

Błędy: 401, 429.

8.3 — Szczegóły projektu

GET/projects/{projectId}

Pełne dane projektu wraz z listą etapów. Pola responsibilities, requirements, offer zawierają treść HTML. minimumSalary/maximumSalary w walucie tenanta (0 = nieustawione).

{ "data": { "id": "ff315ea1-…", "name": "Senior PHP Developer",
    "minimumSalary": 12000, "maximumSalary": 18000, "numberOfVacancies": 4,
    "stages": [
      { "id": "…", "name": "Aplikacja", "isApplicationType": true },
      { "id": "…", "name": "Rozmowa", "isApplicationType": false } ] } }

Błędy: 401, 404 project_not_found, 429.

8.4 — Etapy projektu

GET/projects/{projectId}/stages

Sama tablica etapów (kształt jak data.stages z 8.3). Etap o isApplicationType: true to etap aplikacyjny (max jeden na projekt).

Błędy: 401, 404 project_not_found, 429.

8.5 — Kandydaci w projekcie

GET/projects/{projectId}/candidates?page={n}&stageId={uuid}

Stronicowana lista kandydatów; opcjonalny filtr stageId. Pola: id, firstName, lastName, email, stageId, isRejected, addedAt. Pole email może być puste — od v1.3 aplikację przez API można złożyć bez adresu e-mail (zob. 8.11).

Błędy: 401, 404 project_not_found, 429.

8.6 — Schemat formularza aplikacyjnego ZMIANA v1.2

GET/projects/{projectId}/application-form

Zwraca pełną strukturę formularza (metadane + zagnieżdżone sekcje → pytania → warianty odpowiedzi). Użyj do zbudowania mapy odpowiedzi i pobrania version do POST /apply.

◆ Nowość v1.2 — rozwinięte podformularze

Każda sekcja zwraca teraz tablicę subForms[] z pełną, rozwiniętą strukturą podformularza (np. calendar-days, week-days) — zagnieżdżone sekcje i pytania. Wcześniej dostępne były wyłącznie identyfikatory. Pole subFormIds pozostaje jako powiązanie sekcja→podformularz.

{ "data": {
    "id": "78132d76-…", "projectId": "ff315ea1-…", "schemaId": "d6aea4da-…",
    "formType": "custom", "schemaType": "default",
    "schemaName": { "pl": "Formularz aplikacyjny" },
    "version": "2026-04-09T12:55:57+00:00",
    "isEmailRequired": true, "isPhoneNumberRequired": true, "isCvRequired": true,
    "sections": [
      { "id": "1f0d3c8a-…", "name": { "pl": "Dane podstawowe" }, "order": 0,
        "questions": [
          { "id": "9a2c7e44-…", "type": "oneAnswer", "subtype": null,
            "label": { "pl": "Forma zatrudnienia" }, "isRequired": true, "order": 0,
            "answers": [
              { "id": "b7e1d2a3-…", "content": { "pl": "Umowa o pracę" },
                "isDisqualifying": false, "order": 0 } ] } ],
        "subFormIds": ["30c5a856-…"],
        "subForms": [                          // ◆ v1.2: rozwinięte
          { "id": "30c5a856-…", "type": "week-days",
            "name": { "pl": "W jakich dniach możesz się spotkać?" },
            "sections": [
              { "id": "c1c4ae4c-…", "name": { "pl": "pn." }, "order": 0,
                "questions": [
                  { "id": "4a793138-…", "type": "yesNo", "subtype": "weekDay",
                    "label": { "pl": "pn." }, "isRequired": false, "order": 0 },
                  { "id": "499a4fed-…", "type": "oneAnswer", "subtype": "hourFrom",
                    "label": { "pl": "Od godziny" } } ],
                "subFormIds": [], "subForms": [] } ] } ] } ] } }

Pola sekcji: id, name, description, payload, order, questions[], subFormIds[], subForms[].

Obiekt podformularza (subForms[]): id (zgodny z pozycją w subFormIds sekcji-rodzica), type (np. calendar-days, week-days), name (mapa lokalizacji), sections[] (ta sama struktura co sekcje najwyższego poziomu — rekurencyjnie z questions, subFormIds, subForms).

Obiekt pytania: id (klucz w mapie answers dla POST /apply), type (oneAnswer, multiAnswers, scale, yesNo, open, financialRequirements, languages, file, referral, …), subtype (np. calendarDay, weekDay, hourFrom, hourTo), label, isRequired, order, answers[] (warianty: id — wartość dla oneAnswer/multiAnswerscontent, isDisqualifying, order).

Błędy: 401, 404 project_not_found, 404 application_form_not_found, 429.

8.7 — Wypełniony formularz kandydata

GET/projects/{projectId}/candidates/{candidateId}/application-form?locale={code}

Zwraca wypełniony formularz danego kandydata — sekcje z pytaniami i przesłanymi odpowiedziami.

{ "data": { "projectId": "ff315ea1-…", "candidateId": "87c42b09-…",
    "applicationFormId": "78132d76-…", "submittedAt": "2026-05-27T17:57:48+00:00",
    "formVersion": "2026-04-09T12:55:57+00:00", "locale": "pl",
    "sections": [
      { "title": "Pytania aplikacyjne", "items": [
        { "questionId": "ae8a8a8a-…", "label": "Ocena PHP",
          "type": "scale", "subtype": null, "answer": "1" } ] } ] } }

items[].answer zależy od type (string | int | bool | tablica | null = brak odpowiedzi). Wewnętrzny scoring (obtainedScore, isDisqualified) oraz pytania plikowe są celowo pominięte.

Błędy: 401, 404 project_not_found, 404 application_form_not_found, 429.

8.8 — Profil kandydata

GET/candidates/{candidateId}

Pełny profil (kontakt, doświadczenie, edukacja, umiejętności, języki, certyfikaty). Pola m.in. experience[], education[], skills[], languageSkills[] (cefrLevel A1–C2), certifications[]. Wewnętrzny description CRM jest wykluczony.

Błędy: 401, 404 candidate_not_found, 429.

8.9 — Projekty kandydata

GET/candidates/{candidateId}/projects?page={n}

Stronicowana lista aktywnych projektów, w których uczestniczy kandydat (malejąco po dacie dodania).

Błędy: 401, 404 candidate_not_found, 429.

8.10 — Publiczne notatki kandydata

GET/candidates/{candidateId}/notes?page={n}&projectId={uuid}

Stronicowana lista publicznych notatek. Notatki prywatne nigdy nie są ujawniane. Opcjonalny projectId zawęża do kontekstu projektu. Pola: id, content, createdAt, createdBy, projectId (lub null).

Błędy: 401, 404 candidate_not_found, 429.

8.11 — Aplikacja (utworzenie kandydata) ZMIANA v1.3

POST/projects/{projectId}/apply

Tworzy nowego kandydata i dodaje go do projektu na etapie aplikacyjnym.

⚠ Brak deduplikacji

Każde wywołanie tworzy nowego kandydata — API nie deduplikuje po e-mailu. Aby uniknąć duplikatów, sprawdź najpierw /projects/{id}/candidates. Aplikując bez e-maila (dozwolone od v1.3), tracisz też możliwość deduplikacji po adresie — rozważ własny klucz idempotencji.

Content-Type: multipart/form-data (z plikami) lub application/x-www-form-urlencoded (bez plików).

PoleWym.TypLimitOpis
firstNametakstring255Imię
lastNametakstring255Nazwisko
emailniestring255v1.3E-mail — od v1.3 opcjonalny; jeśli podany, jest walidowany (błędny format → 400 validation_failed). Pominięcie pola tworzy kandydata bez adresu e-mail.
phoneNumberniestring32Telefon (dowolny format)
sourceniestring64Identyfikator źródła; domyślnie external-api
cvniefile5 MBCV kandydata. Zapisywane jako dokument CV.
attachments[]niefile[]3 × 5 MBv1.2Do 3 dodatkowych plików (poza CV), wysyłanych jako powtarzane części multipart attachments[]. Te same dozwolone typy/rozmiar co cv. Każdy zapisywany jako dokument kandydata „Inne".
answersniestring (JSON)Mapa {questionId: wartość} zakodowana jako JSON. Pomiń, by aplikować bez odpowiedzi.
versionwarunk.stringWymagane gdy podano answers. Wartość z pola version w GET …/application-form; niezgodność → 409 stale_form_version.

Mapowanie wartości answers wg typu pytania:

Typ pytaniaWartość w answers
open, yesNo, oneAnswerstring (dla oneAnswerid wariantu odpowiedzi)
scaleinteger
financialRequirementsnumber
multiAnswerstablica id wariantów [id1, id2, …]
calendarDay (subtype; podformularz calendar-days)v1.2data jako string YYYY-MM-DD (np. 2026-06-30)
languages, file, referralniedozwolone — pomiń te pytania w answers

Odpowiedź 201:

{ "candidateId": "4b257ea8-…", "projectId": "ff315ea1-…",
  "answerSheetId": "3c9b1f7e-…" }   // answerSheetId tylko gdy podano poprawne answers; inaczej null
◆ v1.3 — dane podstawowe widoczne w podglądzie formularza

Imię, nazwisko, e-mail i telefon przekazane w POST /apply są zapisywane także w arkuszu odpowiedzi — rekruter widzi je w podglądzie formularza kandydata w Element (wcześniej sekcja danych podstawowych była w podglądzie pusta dla aplikacji z API). Kontrakt żądania i odpowiedzi pozostaje bez zmian. Dotyczy aplikacji złożonych po wdrożeniu v1.3 — wcześniejsze arkusze nie są uzupełniane wstecznie.

Przykład (kontakt + CV + załączniki + odpowiedzi):

curl --user "api+voicebot@acme.local:<hasło>" \
  -F "firstName=Anna" -F "lastName=Nowak" -F "email=anna.nowak@example.com" \
  -F "source=voicebot-inbound" \
  -F "cv=@./cv.pdf" \
  -F "attachments[]=@./portfolio.pdf" -F "attachments[]=@./certyfikat.png" \
  -F 'version=2026-04-09T12:55:57+00:00' \
  -F 'answers={"9a2c7e44-…":"b7e1d2a3-…","cal-day-q-id":"2026-06-30"}' \
  https://acme.elementapp.ai/api/v1-beta/projects/ff315ea1-…/apply

Przykład minimalny (bez e-maila, od v1.3):

curl --user "api+voicebot@acme.local:<hasło>" \
  -F "firstName=Anna" -F "lastName=Nowak" -F "phoneNumber=+48 500 100 200" \
  https://acme.elementapp.ai/api/v1-beta/projects/ff315ea1-…/apply

Błędy: 400 validation_failed, 400 invalid_answers, 401, 404 project_not_found, 409 no_application_stage, 409 stale_form_version, 429.

8.12 — Dodanie notatki do kandydata

POST/candidates/{candidateId}/notes

Dodaje publiczną notatkę (autor: konto API, w UI jako API: <nazwa-systemu>). Content-Type: application/x-www-form-urlencoded. Pola: content (wymagane, do 10 000 znaków), projectId (opcjonalne). Odpowiedź 201: { "noteId": "…" }.

Błędy: 400 validation_failed, 401, 404 candidate_not_found, 429.

9Scenariusze integracji

9.1 — Voice-bot: zgłoszenie aplikacji

Bot dzwoni do kandydata, zbiera dane, składa aplikację (kontakt + CV) i dodaje transkrypcję jako notatkę.

9.2 — Partner ATS: synchronizacja statusów

ATS cyklicznie pobiera statusy kandydatów w projektach; zachowaj odstęp ≥1 s między wywołaniami (limit 70/min).

9.3 — Pipeline danych: eksport odpowiedzi

System analityczny pobiera listę kandydatów, a dla każdego wypełniony formularz (z parametrem locale); odstęp ≥1 s.

10Bezpieczeństwo i higiena

  • Poświadczenia: jeden zestaw na system; hasło w sejfie sekretów; nigdy w repo/czacie; rotacja przez „Wygeneruj nowe hasło" (stare unieważniane natychmiast).
  • Idempotencja (po stronie integratora): API nie deduplikuje POST /apply. Strategie: sprawdź kandydatów po e-mailu przed wysłaniem (jeśli go zbierasz — od v1.3 e-mail jest opcjonalny), loguj candidateId przed oznaczeniem operacji jako zakończonej, lub użyj wzorca outbox z kluczem idempotencji.
  • Backoff: wykładniczy od 1 s (limit 30 s) na 429; rozkładaj operacje wsadowe w czasie; unikaj równoległych żądań na jednym poświadczeniu.
  • RODO/GDPR: dane kandydatów to dane osobowe (obowiązki po Twojej stronie). Element zwraca 404 candidate_not_found dla kandydatów usuniętych RODO — usuń też swoją kopię.

11Słownik

  • Tenant — klient Element (firma); identyfikowany po subdomenie; poświadczenia zablokowane do jednego tenanta.
  • Projekt — proces rekrutacyjny na jedno stanowisko; ma listę etapów.
  • Etap — krok rekrutacji (Aplikacja, Rozmowa, Oferta…). Etap aplikacyjny (isApplicationType: true) — miejsce lądowania nowych zgłoszeń, max jeden na projekt.
  • Kandydat — osoba w CRM Element; może uczestniczyć w wielu projektach.
  • Podformularz (sub-form) — zagnieżdżony blok pytań w sekcji (np. calendar-days, week-days). Od v1.2 zwracany rozwinięty w subForms[].
  • Notatka publiczna / prywatna — publiczne widoczne dla konsultantów tenanta i dostępne przez API; prywatne nigdy nie eksponowane.
  • RODO / zapomniany — kandydat zażądał usunięcia danych; zwraca 404.

12Zakres i roadmapa

Dostępne w v1-beta. Odczyt: projekty, etapy, kandydaci w projekcie, pełny schemat formularza (z rozwiniętymi podformularzami), wypełnione odpowiedzi kandydata, profil kandydata, projekty kandydata, publiczne notatki. Zapis: zgłoszenie aplikacji (nowy kandydat; wymagane tylko imię i nazwisko — e-mail, telefon, CV, załączniki i odpowiedzi opcjonalne), publiczna notatka.

Planowane (po v1-beta). Zmiana etapu kandydata, odrzucenie, aktualizacja scoringu, dodanie istniejącego kandydata do projektu, utworzenie projektu.

Celowo pominięte. Notatki prywatne (odczyt/zapis), wewnętrzny description CRM, wewnętrzne pola scoringu (obtainedScore, isDisqualified), odpowiedzi na pytania typu file/referral/languages w POST /apply.

◆ Polityka beta

Kontrakt może zmienić się niekompatybilnie w trakcie bety (~2–3 mies. użycia produkcyjnego); zmiany łamiące komunikowane z wyprzedzeniem. Dodatki minorowe (nowe opcjonalne pola, endpointy, kody błędów) nie są łamiące — Twój klient HTTP powinien tolerować dodatkowe pola.

13Kontakt i wsparcie

Zgłoszenia przez standardowy kanał wsparcia Element. Dołącz: subdomenę, login API, znacznik czasu UTC wywołania, metodę + ścieżkę, status HTTP, treść błędu (przynajmniej error.code i error.message) oraz własny identyfikator korelacji. Te dane pozwalają szybko odnaleźć żądanie w logu audytowym.