Dokumentacja API

Zasoby

Pracownicy

Odczyt kartotek, eksport gotowych plików kadrowych, aktualizacja pól i dwukierunkowa synchronizacja z programem kadrowym. Zapisy nie tworzą nowych pracowników, aktualizują dopasowanych (merge zachowawczy).

Klucz obejmuje jedną spółkę

Klucz API wystawia się dla konkretnej spółki (płatnika) i widzi wyłącznie jej dane: pracowników, listy płac, godziny i faktury. Jeśli obsługujesz kilka spółek, wystaw osobny klucz dla każdej. Podanie w zapytaniu identyfikatora innej spółki zwraca 403 z kodemcompany_out_of_scope.

Model kanoniczny

W środku utrzymujemy jeden kanoniczny model pracownika, z którego budowane są wszystkie eksporty. Import i PATCH nigdy nie zerują danych, uzupełniają tylko puste lub zmienione pola. Zakładanie pracownika od zera odbywa się osobnym przepływem onboardingu.

GET/employees

read:employees

Lista pracowników tenanta (dane podsumowujące, bez odszyfrowanego PII). Odpowiedź jest stronicowana.

Domyślnie zwracamy tylko osoby zatrudnione: obecnych pracowników i tych, którym zakończono zatrudnienie. Zgłoszenia w trakcie onboardingu, anulowane i takie, którym wygasł link, nie wychodzą przez API. Pełną listę, razem z lejkiem rekrutacyjnym, zwraca include=all.

Parametry zapytania

include
stringopcjonalne
employees (domyślne) albo all. 'all' dokłada zgłoszenia w trakcie onboardingu, anulowane i wygasłe.
status
stringopcjonalne
filtr po jednym statusie, np. podpisany, zakonczony, oczekuje_kontrasygnaty, wygasly. Wartości są takie jak w panelu, po polsku.
companyId
uuidopcjonalne
ogranicz do jednej firmy (płatnika). Klucz i tak widzi wyłącznie swoją spółkę; podanie innej zwraca 403 company_out_of_scope.
updatedSince
ISO-8601opcjonalne
tylko rekordy zmienione od podanego momentu, do synchronizacji przyrostowej.
page
integeropcjonalne
numer strony (domyślnie 1).
pageSize
integeropcjonalne
rozmiar strony (domyślnie 50, maks. 200).
Żądanie · cURL
curl -H "Authorization: Bearer onb_..." \
  "https://www.onboardly.work/api/v1/employees?updatedSince=2026-07-01&pageSize=50"
Odpowiedź 200 · JSON
{
  "data": [{
    "id": "uuid", "firstName": "Jan", "lastName": "Kowalski",
    "email": "jan@example.com", "phone": "+48…", "status": "podpisany",
    "contractType": "zlecenie", "isForeigner": false, "nationalityType": null,
    "position": "Kelner", "companyId": "uuid",
    "startDate": "2026-06-01", "endDate": null,
    "salaryNet": null, "salaryGross": 4806, "salaryKind": "brutto",
    "hourlyRate": null, "hourlyRateKind": null,
    "workTime": { "numerator": 1, "denominator": 2 },
    "employmentContractKind": "okreslony",
    "projects": [{ "id": "uuid", "name": "Bocca", "clientName": "OCTO", "dateFrom": "2026-06-01", "dateTo": null }],
    "projectName": "Bocca",
    "updatedAt": "2026-07-10T12:00:00.000Z"
  }],
  "page": 1, "pageSize": 50, "total": 130, "totalPages": 3
}

GET/employees (blok podatkowy)

read:employees

Każdy wiersz listy niesie dodatkowo blok podatkowy: adresy, urząd skarbowy, pełne oświadczenie PIT-2 i dane o rozwiązaniu umowy. Nie wymaga to nowego klucza ani zmiany uprawnień — pola dochodzą do odpowiedzi, której kształt znacie, więc integracja czytająca wybrane pola działa bez zmian.

Dodatkowe pola w każdym wierszu · JSON
{
  "address": {
    "street": "Hetmańska", "houseNumber": "25", "apartmentNumber": null,
    "postalCode": "15-727", "city": "Białystok", "post": "Białystok",
    "gmina": "Białystok", "powiat": "Białystok", "voivodeship": "podlaskie",
    "country": "PL"
  },
  "registeredAddress": null, "correspondenceAddress": null,
  "taxAddressSource": "residence",
  "taxOffice": { "name": "Urząd Skarbowy w Białymstoku", "code": "2001" },
  "nip": null,
  "pit2": {
    "applied": true, "mode": "1/12", "declared": true, "source": "wizard",
    "declarations": {
      "reliefReturn": false, "reliefReturnFrom": null, "reliefReturnTo": null,
      "relief4plus": false, "reliefSenior": false,
      "jointSpouse": false, "jointIncomeVariant": null, "singleParent": false,
      "noAdvance": false, "noAdvanceYear": null,
      "resignYoungRelief": false, "resignKup": false, "resign50Kup": false,
      "withdrawReduction": false, "withdrawJoint": false, "withdrawElevatedKup": false,
      "withdrawExemptions": false, "withdrawYoungRelief": false, "withdrawKup": false,
      "withdraw50Kup": false, "withdrawNoAdvance": false,
      "art42eMode": null, "art42eWithdraw": null,
      "commuteKup": false
    }
  },
  "ppkResign": false,
  "ppk": { "resigned": false, "joinedAt": "2026-07-01", "resignedAt": null },
  "termination": {
    "type": "dyscyplinarne",
    "typeLabel": "Rozwiązanie umowy bez wypowiedzenia (art. 52 KP)",
    "date": "2026-09-30",
    "zusDeregistrationCode": "100",
    "optimaCode": "7"
  }
}

Na co uważać

pit2.applied
booleanopcjonalne
czy stosujemy kwotę zmniejszającą u tego płatnika. To jest pole decydujące o naliczeniu.
pit2.declared
booleanopcjonalne
czy w systemie jest udokumentowane oświadczenie. UWAGA: declared = true przy applied = false to poprawny stan — pracownik złożył oświadczenie, w którym NIE wnosi o stosowanie PIT-2 u nas (typowe przy zleceniu i dziele albo gdy korzysta z ulgi u innego pracodawcy).
pit2.mode
enumopcjonalne
ułamek kwoty zmniejszającej: 1/12, 1/24 albo 1/36. Wypełniony TYLKO gdy applied = true. null przy applied = false nie znaczy braku oświadczenia — o tym mówi declared.
pit2.source
enumopcjonalne
skąd mamy oświadczenie: wizard (kreator), hr (teczka), paper (deklaracja pisemna). null = deklaracji nie ma, a applied pochodzi ze starej kolumny z wartością domyślną; wtedy declared = false.
pit2.declarations
objectopcjonalne
oświadczenia z wzoru PIT-2(9). noAdvanceYear mówi, w którym roku złożono wniosek z części J — obowiązuje tylko w nim. jointSpouse i resignKup dotyczą wyłącznie umowy o pracę.
pit2.thresholdMode
enumopcjonalne
pole „Progi podatkowe" z kartoteki Optimy, wyliczone z części E wzoru PIT-2: `standardowe` albo `malzonek_lub_samotny_rodzic`. Trzecia wartość słownika Optimy, `indywidualne`, **nigdy nie wychodzi z Onboardly**: oznacza ręcznie ustawione własne progi na karcie pracownika, a takiej funkcji u nas nie ma. Zostaje w wykazie, żeby mapowanie pokrywało pełny słownik.
pit2.declarations.withdraw*
booleanopcjonalne
wycofania uprzednio złożonych oświadczeń (poz. 7, 11, 13, 15, 17, 19, 21). Wzór nie zna „odznaczenia": zmiana deklaracji następuje przez złożenie nowej, a wycofanie jest osobną pozycją. Bez tych pól nie odróżnisz „nic nie złożył" od „złożył i wycofał", a to są dwa różne stany przy naliczaniu zaliczki.
pit2.declarations.art42eMode
enumopcjonalne
część D wzoru: pomniejszenie u INNEGO płatnika z art. 42e (organ egzekucyjny albo podmiot przejmujący zobowiązania zakładu pracy). Dopuszczalne wyłącznie 1/24 i 1/36, bo 1/12 taki płatnik stosuje z urzędu. null = nie złożono. My tego oświadczenia nie przyjmujemy, pole służy odnotowaniu, że istnieje, bo wpływa na łączną kwotę pomniejszenia.
pit2.declarations.art42eWithdraw
enumopcjonalne
część D poz. 9: „split" = wycofanie oświadczenia o podziale kwoty zmniejszającej, „resign_1_12" = rezygnacja ze stosowania 1/12. Pozycja dopuszcza jedno z dwóch, nie oba.
ppk
objectopcjonalne
resigned to dotychczasowa flaga tak/nie, joinedAt i resignedAt to daty w formacie YYYY-MM-DD. Wpłaty nalicza się OD DATY, sama flaga nie wystarcza. null znaczy „nie mamy daty", nie „nie było zdarzenia": nie podstawiamy w to miejsce daty zatrudnienia. Rezygnacja nie kasuje daty przystąpienia: oba zdarzenia zaszły naprawdę.
ppkResign
booleanopcjonalne
zostaje dla kluczy, które już to czytają. Ta sama wartość co ppk.resigned.
schoolType
enumopcjonalne
rodzaj ukończonej szkoły: podstawowa, zasadnicza_zawodowa, srednia_ogolnoksztalcaca, srednia_zawodowa, policealna, wyzsza. Uszczegóławia `educationLevel`, bo „średnie" to w prawie dwie różne szkoły.
schoolYearsForLeave
numberopcjonalne
lata nauki wliczane do stażu urlopowego (art. 155 § 1 KP): 3, 4, 5, 6 albo 8. Dla szkoły podstawowej **0, i to jest wartość prawdziwa**, nie brak danych; brak danych to `null`. Okresów nauki się NIE sumuje, liczy się jedna ukończona szkoła.
powiat
stringopcjonalne
uzupełniany z kodu pocztowego przy trafieniu dokładnym, tak jak miasto i gmina. Przy dopasowaniu po dwucyfrowym prefiksie zostaje pusty. Teczki sprzed 25.09.2026 mają go pustego, dopóki adres nie zostanie zapisany ponownie.
termination.date
dateopcjonalne
faktyczne rozwiązanie; bywa różne od endDate, które opisuje koniec umowy terminowej znany z góry. Dla ZUS to nie to samo zdarzenie. null dopóki umowa trwa.
termination.zusDeregistrationCode
stringopcjonalne
kod przyczyny wyrejestrowania na ZUS ZWUA: 100 = ustanie tytułu, 500 = zgon.
termination.type
enumopcjonalne
przyczyna rozwiązania umowy. Lista ma 36 wartości: 35 odpowiada pozycjom słownika Comarch ERP Optima, a siedem kluczy sprzed rozszerzenia zostało, bo wiszą na nich zamknięte teczki. Mapuj po `optimaCode`, nie po nazwie klucza.
termination.optimaCode
stringopcjonalne
numer pozycji w słowniku Optimy, od 1 do 35. `null` dla teczek zamkniętych przed wprowadzeniem słownika. Uwaga: dwie wartości wskazują na pozycję 1, bo historyczne „wypowiedzenie bez podania przyczyny" to u Optimy zwykłe wypowiedzenie przez pracodawcę.
registeredAddress
objectopcjonalne
wypełniony tylko gdy pracownik zadeklarował adres inny niż zamieszkania. null nie znaczy „brak danych", tylko „taki sam jak zamieszkania".

GET/employees/{id}

read:employees

Pełna kartoteka kanoniczna z odszyfrowanymi danymi osobowymi. Każde wywołanie loguje dostęp do PII (RODO art. 30).

Odpowiedź niesie też wszystko, co wisi przy pracowniku: assignments (przypisania projektowe ze stawką), documents (metadane teczki, bez zawartości plików), deductions (potrącenia), wageComponents (składniki wynagrodzenia z flagami zusable i taxable), benefits, family (członkowie rodziny zgłoszeni do ZUS), trainings, leave oraz vacationBalance.

Odpowiedź 200 (fragment) · JSON
{
  "data": {
    "onboardingId": "uuid", "status": "podpisany",
    "firstName": "Jan", "lastName": "Kowalski", "email": "…", "phone": "…",
    "pesel": "86010112345", "birthDate": "1986-01-01", "placeOfBirth": "Warszawa",
    "nationality": "PL", "isForeigner": false,
    "residenceAddress": { "street": "…", "houseNumber": "…", "postalCode": "00-001", "city": "Warszawa", "gmina": "…", "powiat": "…", "voivodeship": "mazowieckie", "country": "PL" },
    "registeredAddress": null, "correspondenceAddress": null, "taxAddressSource": "residence",
    "contractType": "zlecenie", "position": "Kelner", "educationLevel": "srednie",
    "schoolType": "srednia_zawodowa", "schoolYearsForLeave": 5,
    "occupationCode": "941201", "startDate": "2026-06-01", "endDate": null,
    "workTime": null, "employmentContractKind": null, "probationEndDate": null,
    "hourlyRate": 30.0, "bankAccount": "PL…", "bankName": "mBank",
    "insurance": { "code": "041100", "confidence": "low", "requiresZusRegistration": true, "document": "ZUA" },
    "nfzCode": "07", "pit2": true, "pit2Mode": "1/12", "pit2Declared": true, "pit2Source": "wizard",
    "pit2Declarations": { "reliefReturn": false, "noAdvance": false, "…": false },
    "ppkResign": false, "ppkJoinedAt": "2026-07-01", "ppkResignedAt": null,
    "taxOfficeName": "…", "taxOfficeCode": "…",
    "terminationType": null, "terminationDate": null, "zusDeregistrationCode": null,
    "employer": { "name": "…", "nip": "…", "regon": "…", "address": "…" }
  }
}
HTTPcodeZnaczenie
404not_foundbrak pracownika w tym tenancie.
410pii_deleteddane osobowe usunięte (RODO).

GET/worker-hours/{yearMonth}

read:payroll

Godziny zgłoszone przez pracowników za dany miesiąc, czyli raporty z portalu pracownika. To źródło, a nie wynik: /payroll/{yearMonth} zwraca wyliczone listy płac, a /settlements/{id} zbiorcze rozliczenie projektu. Filtry: companyId, projectId, status, onboardingId.

Odpowiedź 200 · JSON
{
  "yearMonth": "2026-09",
  "data": [{
    "id": "uuid", "onboardingId": "uuid", "worker": "Jan Kowalski",
    "companyId": "uuid", "contractType": "zlecenie", "yearMonth": "2026-09",
    "status": "approved",
    "totalHours": 168.5, "vacationDays": 2, "sickDays": 0,
    "netAmountOverride": null,
    "byProject": [
      { "projectId": "uuid", "projectName": "Sartoria — Centrum", "hours": 120 },
      { "projectId": "uuid", "projectName": "Hotel — Housekeeping", "hours": 48.5 }
    ],
    "submittedAt": "2026-10-02T09:12:00.000Z",
    "approvedAt": "2026-10-03T11:40:00.000Z", "approvedBy": "kadry@firma.pl"
  }],
  "count": 1
}

Na co uważać

status
enumopcjonalne
draft | submitted | client_approved | client_rejected | approved | rejected. Do rozliczeń bierz approved; submitted czeka jeszcze na akceptację.
byProject
arrayopcjonalne
podział godzin na projekty, liczony z wpisów dziennych (tylko typ „praca"). Raport miesięczny jest jeden na pracownika, więc bez tego nie rozliczysz miesiąca z pracą na dwóch projektach. Suma pozycji może być mniejsza niż totalHours, gdy część dni nie ma wskazanego projektu.
netAmountOverride
numberopcjonalne
kwota netto wpisana ręcznie przez kadry; wygrywa z wyliczeniem godziny × stawka. null = licz ze stawki.

GET/employees/{id}/export?format=

read:employees

Zwraca gotowy plik (nie JSON) dla programu kadrowego. Nagłówek Content-Disposition: attachment; ewentualne ostrzeżenia w nagłówku X-Payroll-Warnings (URL-encoded JSON).

Parametr zapytania

format
enumopcjonalne
kedu | optima | symfonia | csv | xlsx (domyślnie kedu). KEDU to uniwersalne zgłoszenie ZUS czytane przez Płatnik, Optima, Symfonia, enova i Gratyfikant.
cURL
curl -H "Authorization: Bearer onb_..." \
  "https://www.onboardly.work/api/v1/employees/{id}/export?format=kedu" -o kedu.xml
HTTPcodeZnaczenie
400—nieznany format.
422incompletebrak wymaganych pól, ciało zawiera { blockers[], warnings[] }.
501format_not_readyformat jeszcze niegotowy dla tej ścieżki.

GET/employees/{id}/ift?year=

read:employees

Dane do informacji IFT-1/IFT-1R (nierezydent): dane osobowe, zagraniczny TIN, adres zagraniczny, rachunek (IBAN + SWIFT/BIC), certyfikat rezydencji, dokumenty legalizacyjne z datami ważności oraz suma przychodów i pobranego podatku z rachunków wskazanego roku.

Parametr zapytania

year
stringopcjonalne
Rok podatkowy (domyślnie bieżący), np. 2026.
cURL
curl -H "Authorization: Bearer onb_..." \
  "https://www.onboardly.work/api/v1/employees/{id}/ift?year=2026"
HTTPcodeZnaczenie
400not_foreignerpracownik nie jest cudzoziemcem: IFT nie dotyczy.
404not_foundnie znaleziono pracownika.

POST/employees/import-result

write:employees

Informacja zwrotna z importera: wynik zapisu jednej kartoteki w zewnętrznym systemie kadrowym. Jedno wywołanie oznacza jedną kartotekę.

Body · JSON
{
  "employeeId": "88ab2b33-6849-46ec-8d93-eca5e0446873",
  "status": "error",
  "message": "Niepoprawny numer PESEL, kartoteka nie została zapisana.",
  "system": "optima"
}

Pola

employeeId
UUIDopcjonalne
identyfikator pracownika w Onboardly, czyli onboardingId z GET /employees.
status
enumopcjonalne
ok albo error. Przy ok komunikat jest pomijany.
message
stringopcjonalne
opis błędu. Nazwa pola i opis problemu, bez wartości danych osobowych. Dłuższy niż 500 znaków jest przycinany.
system
stringopcjonalne
opcjonalnie, nazwa systemu (np. optima). Pozwala rozróżnić źródła, gdy dojdzie drugie.

Na co uważać

Zatarcie danych osobowych
serweropcjonalne
z `message` usuwamy ciągi wyglądające na PESEL, numer konta, NIP, REGON i adres e-mail, zastępując je [usunięto]. Kolumna z komunikatem nie jest szyfrowana i trafia na ekran bez kontroli dostępu do PII. Nazwa pola i opis problemu zostają nietknięte, a odpowiedź oddaje `message` PO zatarciu, żeby było widać, co zapisaliśmy.
Pętla poprawek
procesopcjonalne
przy status: error kadry widzą błąd w trzech miejscach: pasek w teczce, oznaczenie na liście pracowników i powiadomienie na pulpicie. Po poprawieniu danych rośnie updatedAt kartoteki, więc importer może ponowić zapis.
Odhaczenie przez kadry
procesopcjonalne
każdy kolejny wynik, także ok, kasuje wcześniejsze odhaczenie. Odhaczenie sprzed tygodnia nie ukryje dzisiejszego błędu, a udany zapis gasi powiadomienie sam.

PATCH/employees/{id}

write:employees

Aktualizacja pól kadrowych (kolumny płaskie). Wszystkie pola opcjonalne. PII i adres nie są edytowane tą drogą.

Ciało żądania

position
stringopcjonalne
stanowisko.
salaryNet
numberopcjonalne
wynagrodzenie netto.
hourlyRate
numberopcjonalne
stawka godzinowa.
startDate / endDate
YYYY-MM-DDopcjonalne
daty zatrudnienia.
nfzCode / nfzBranch
stringopcjonalne
kod i oddział NFZ.
usName / usCode
stringopcjonalne
nazwa i kod urzędu skarbowego.
bankName
stringopcjonalne
nazwa banku.
nip
stringopcjonalne
NIP pracownika (B2B / PIT-11).
pit2 / ppkResign
booleanopcjonalne
oświadczenie PIT-2 / rezygnacja z PPK. Uwaga: ułamek kwoty zmniejszającej (pit2Mode) i oświadczenia z PIT-2(9) są tylko do odczytu — zmienia je pracownik w portalu albo kadry w teczce.
ppkJoinedAt / ppkResignedAt
YYYY-MM-DDopcjonalne
daty przystąpienia do PPK i rezygnacji z wpłat. Pusty string kasuje datę. Przy onboardingu ustawia je data złożenia oświadczenia, więc zapis zwrotny jest potrzebny głównie dla deklaracji papierowych.
cURL
curl -X PATCH -H "Authorization: Bearer onb_..." -H "Content-Type: application/json" \
  -d '{"nfzCode":"07","bankName":"mBank","pit2":true}' \
  "https://www.onboardly.work/api/v1/employees/{id}"
Odpowiedź 200 · JSON
{ "ok": true, "updated": ["nfzCode", "bankName", "pit2"] }
HTTPcodeZnaczenie
400invalidnieprawidłowa wartość pola.
400emptybrak pól do aktualizacji.
404not_foundbrak pracownika w tym tenancie.

POST/employees/import

write:employees

Podgląd importu, nic nie zapisuje. Parsuje plik i dopasowuje rekordy po PESEL (fallback: imię + nazwisko + data urodzenia).

Ciało żądania

format
enumwymagane
kedu | symfonia | optima | csv | xlsx.
content
stringwymagane
treść pliku (maks. 5 MB).
cURL
curl -X POST -H "Authorization: Bearer onb_..." -H "Content-Type: application/json" \
  -d '{"format":"kedu","content":"<KEDU>...</KEDU>"}' \
  "https://www.onboardly.work/api/v1/employees/import"
Odpowiedź 200 · JSON
{
  "total": 12, "matched": 10, "new": 2,
  "rows": [{
    "name": "Jan Kowalski", "pesel": "860…",
    "matchedOnboardingId": "uuid", "match": "matched"
  }]
}
HTTPcodeZnaczenie
422parse_failednie udało się sparsować pliku.
501format_not_readyparser formatu niegotowy.

POST/employees/import/confirm

write:employees

Zapis importu, merge zachowawczy (uzupełnia tylko puste lub zmienione pola, nigdy nie zeruje). Upsert wyłącznie do dopasowanych i potwierdzonych identyfikatorów. Nie tworzy nowych pracowników.

Ciało żądania

format
enumwymagane
jak w podglądzie.
content
stringwymagane
treść pliku.
confirmIds
uuid[]wymagane
identyfikatory dopasowanych pracowników do zapisania.
Odpowiedź 200 · JSON
{
  "updated": 2, "skipped": 1,
  "updatedDetail": [{ "onboardingId": "uuid", "fields": ["personal.pesel", "nfzCode"] }],
  "skippedDetail": [{ "reason": "brak zmian", "pesel": "…" }]
}

POST/employees/{id}/zus/deregister

write:employees

Ustawia datę zakończenia zatrudnienia i zwraca plik KEDU ZWUA (wyrejestrowanie z ZUS). Wymaga wcześniejszego zgłoszenia (ZUA/ZZA).

Ciało żądania

endDate
YYYY-MM-DDwymagane
data zakończenia zatrudnienia.
cURL
curl -X POST -H "Authorization: Bearer onb_..." -H "Content-Type: application/json" \
  -d '{"endDate":"2026-07-31"}' \
  "https://www.onboardly.work/api/v1/employees/{id}/zus/deregister" -o zwua.xml
HTTPcodeZnaczenie
409not_registeredB2B / dzieło, brak zgłoszenia, nie ma czego wyrejestrować.
410pii_deleteddane osobowe usunięte (RODO).

Uwagi implementacyjne

  • • Eksport KEDU obsługuje ZUA/ZZA; RUD (dzieło) jest pomijany (składa się przez PUE ZUS); ZWUA wyłącznie przez endpoint wyrejestrowania.
  • • Kod tytułu ubezpieczenia dla zleceń bywa confidence: "low", potwierdź go przed złożeniem w ZUS.
  • • Import i PATCH nie zakładają pracowników od zera, do tego służy przepływ onboardingu.