ZatrudnijMnie API Documentation
Version: 1.0
Base URL: https://zatrudnijmnie.pl/api/v1
Authentication: API Key (Bearer token)
Spis treści
- Wprowadzenie
- Autoryzacja
- Uprawnienia
- Rate Limiting
- CORS
- Format odpowiedzi
- Walidacja parametrów
- Endpoints
- Webhooki – payload i sygnatura
- Słowniki wartości
- Limit pobrań danych kontaktowych
- Kody błędów
- Przykłady
Wprowadzenie
API ZatrudnijMnie umożliwia programowy dostęp do ogłoszeń kandydatów szukających pracy. Dostępne jest dla wszystkich Kont Zespołowych.
Dostępność API według planów
| Plan | API Access | Limit aktywnych kluczy | Limit pobrań kontaktów / 30 dni | Webhooki |
|---|---|---|---|---|
| Team Basic | ✅ | 2 | 250 | ❌ |
| Team Premium | ✅ | 5 | 1 000 | ❌ |
| Team Premium Plus | ✅ | 10 | 2 000 | ✅ |
| Team Enterprise | ✅ | 50 | 5 000 lub limit indywidualny | ✅ |
Rate limit jest konfigurowany per klucz (domyślnie 1 000 req/h i 10 000 req/dzień; maksymalnie 10 000 req/h i 100 000 req/dzień). Webhooki (
webhooks.manage) są dostępne od planu Team Premium Plus – ograniczenie jest wymuszane server-side niezależnie od uprawnień zapisanych w kluczu.
Limit pobrań danych kontaktowych to co innego niż rate limit. Rate limit ogranicza liczbę żądań, limit pobrań – liczbę kandydatów, których dane osobowe zostały ujawnione. Szczegóły: Limit pobrań danych kontaktowych.
Dostęp jest odbierany automatycznie, gdy dostęp organizacji wygaśnie lub zostanie zawieszony – wszystkie klucze przestają wtedy działać (API_KEY_INVALID).
Autoryzacja
API wymaga klucza API przekazywanego w nagłówku HTTP.
Metody autoryzacji
1. Bearer Token (zalecane):
Authorization: Bearer zm_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2. X-API-Key Header:
X-API-Key: zm_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Nazwy nagłówków są niewrażliwe na wielkość liter.
⚠️ Przekazywanie klucza w parametrze query (
?api_key=...) jest zabronione – takie żądania są odrzucane (API_KEY_MISSING) i logowane jako podejrzana aktywność. Klucz w URL wycieka do logów serwerów, proxy i historii przeglądarki.
Format klucza
Klucz ma postać zm_live_ + 48 znaków szesnastkowych (łącznie 56 znaków) i jest walidowany wyrażeniem ^zm_live_[a-f0-9]{48}$. Nie istnieje osobne środowisko testowe (zm_test_) – klucze zawsze działają na danych produkcyjnych.
Generowanie klucza API
- Zaloguj się do konta zespołu
- Przejdź do Ustawienia → Dostęp / API
- Kliknij "Utwórz nowy klucz"
- Zapisz klucz natychmiast – nie będzie można go ponownie wyświetlić!
W bazie przechowywany jest wyłącznie skrót SHA-256 klucza oraz jego 8-znakowy prefiks (do identyfikacji w panelu). Utracony klucz można jedynie zregenerować, co dezaktywuje poprzedni.
Bezpieczeństwo kluczy
- ⚠️ Nigdy nie udostępniaj klucza API publicznie
- ⚠️ Nie commituj kluczy do repozytorium
- ⚠️ Używaj zmiennych środowiskowych
- ✅ Regeneruj klucze regularnie
- ✅ Używaj osobnych kluczy dla różnych środowisk
- ✅ Nadawaj kluczom minimalny wymagany zestaw uprawnień (patrz niżej)
Uprawnienia
Każdy klucz API ma zestaw uprawnień ustawiany przy tworzeniu i edycji w panelu zespołu. Brak wymaganego uprawnienia skutkuje odpowiedzią 403 PERMISSION_DENIED.
| Uprawnienie | Wymagane przez | Domyślnie |
|---|---|---|
listings.read |
GET /listings, GET /listings/:id |
✅ włączone |
contacts.read |
GET /listings/:id/cv oraz pola contact i cvUrl w GET /listings/:id |
❌ wyłączone (wymaga jawnego zaznaczenia) |
analytics.read |
GET /analytics, GET /analytics/usage |
✅ włączone |
webhooks.manage |
wszystkie endpointy /webhooks |
❌ wyłączone (Team Premium Plus i wyżej) |
Endpointy słownikowe (/positions, /cities, /contract-types) wymagają wyłącznie ważnego klucza – nie sprawdzają żadnego uprawnienia.
Least-privilege: dostęp do danych kontaktowych kandydata i CV wymaga jawnego uprawnienia
contacts.read. Uprawnienielistings.readsamo w sobie nie otwiera dostępu do danych osobowych – klucz bez zapisanej sekcjicontactsotrzyma403 PERMISSION_DENIEDnaGET /listings/:id/cv, aGET /listings/:idzwróci ogłoszenie bez pólcontacticvUrl. Jeśli klucz ma korzystać z danych kontaktowych, zapisz jego uprawnienia w panelu zespołu z zaznaczonymcontacts.read.
webhooks.managejest weryfikowane dwuetapowo: klucz musi mieć to uprawnienie i organizacja musi mieć planteam_premium_plusalboteam_enterprise. Obniżenie planu natychmiast blokuje endpointy webhooków, nawet jeśli uprawnienie pozostało w kluczu.
Limit pobrań danych kontaktowych
Każdy plan zawiera limit unikalnych kandydatów, których dane kontaktowe (i CV) można pobrać przez API w oknie 30 dni kroczących.
Czym różni się od rate limitu: rate limit ogranicza liczbę żądań HTTP, ten limit – liczbę osób, których dane osobowe zostały ujawnione. Można wykonać tysiące żądań listy ogłoszeń bez naruszenia limitu pobrań.
Zasady liczenia
- Okno 30 dni kroczących, niezależnie od długości wykupionego okresu dostępu. Dostęp roczny nie daje dwunastokrotności limitu.
- Unikalni kandydaci – ten sam kandydat pobrany ponownie w oknie nie zużywa kolejnej jednostki. Ponowne pobranie jest możliwe także po wyczerpaniu limitu.
- Zakres: organizacja, nie klucz. Wszystkie klucze zespołu dzielą jedną pulę.
- Liczą się wyłącznie pobrania przez API. Praca w panelu webowym nie jest w żaden sposób ograniczana.
Które wywołania zużywają limit
| Endpoint | Zużywa jednostkę |
|---|---|
GET /listings |
❌ nie – lista nie zawiera danych kontaktowych |
GET /listings/:id z uprawnieniem contacts.read |
✅ tak, o ile kandydat nie był pobrany w bieżącym oknie |
GET /listings/:id bez contacts.read |
❌ nie – odpowiedź nie zawiera pól contact ani cvUrl |
GET /listings/:id/cv |
✅ tak, o ile kandydat nie był pobrany w bieżącym oknie |
/positions, /cities, /contract-types, /analytics |
❌ nie |
Nagłówki
Zwracane przy każdej odpowiedzi endpointów ujawniających dane kontaktowe – również przy odmowie:
X-Contact-Quota-Limit: 1000
X-Contact-Quota-Remaining: 847
X-Contact-Quota-Reset: 2026-09-27T00:00:00.000Z
X-Contact-Quota-Reset to moment, w którym z okna wypadnie najstarsze pobranie – wtedy zwolni się pierwsza jednostka.
Po przekroczeniu limitu
{
"success": false,
"error": {
"code": "CONTACT_QUOTA_EXCEEDED",
"message": "Wyczerpano limit pobrań danych kontaktowych (1000 na 30 dni). Limit odnowi się 27.09.2026.",
"used": 1000,
"limit": 1000,
"resetAt": "2026-09-27T00:00:00.000Z"
},
"timestamp": "2026-08-31T10:12:33.000Z"
}
Status 402 Payment Required – odróżnialny od 403 PERMISSION_DENIED (brak uprawnienia) i 429 RATE_LIMIT_EXCEEDED (zbyt wiele żądań).
Pakiety dokupowe
Właściciel zespołu może dokupić pakiet +250 pobrań w zakładce API panelu zespołu. Pakiet jest ważny 30 dni od zakupu, a niewykorzystane pobrania przepadają wraz z jego wygaśnięciem. Zakup jest jednorazowy – bez subskrypcji i automatycznych odnowień.
Rate Limiting
API stosuje limity requestów na godzinę i dzień, konfigurowane per klucz API.
Limity są egzekwowane trwale (persistent) w oparciu o logi requestów w bazie danych, co zapewnia spójność limitów w środowiskach wieloinstancyjnych.
Wartości domyślne i maksymalne:
| Ustawienie | Domyślnie | Maksimum |
|---|---|---|
rate_limit_per_hour |
1 000 | 10 000 |
rate_limit_per_day |
10 000 | 100 000 |
Nagłówki odpowiedzi
Zwracane przy każdej odpowiedzi po pomyślnej autoryzacji klucza – w tym przy 429 – i udostępnione przeglądarkom przez Access-Control-Expose-Headers. Odpowiedzi 401 (brak lub nieprawidłowy klucz) tych nagłówków nie zawierają:
X-RateLimit-Limit-Hour: 1000
X-RateLimit-Limit-Day: 10000
X-RateLimit-Remaining-Hour: 995
X-RateLimit-Remaining-Day: 9990
Przekroczenie limitu
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Przekroczono limit requestów (godzinowy). Spróbuj ponownie za 3600 sekund.",
"retryAfter": 3600
},
"timestamp": "2026-01-01T12:00:00.000Z"
}
Komunikat wskazuje, który limit został wyczerpany (godzinowy albo dzienny). Nagłówek Retry-After zawiera czas w sekundach do odnowienia limitu.
CORS
API v1 może być używane zarówno z backendów (server-to-server), jak i z aplikacji przeglądarkowych.
Lista dozwolonych originów jest jawną konfiguracją – nie jest używany wildcard *.
Kolejność ustalania listy:
- zmienna
API_V1_CORS_ORIGINS(originy rozdzielone przecinkami) – konfiguracja dedykowana dla publicznego API, - w razie jej braku – główna zmienna
CORS_ORIGIN, - w środowisku deweloperskim, gdy żadna nie jest ustawiona –
http://localhost:3000ihttp://127.0.0.1:3000, - w produkcji/stagingu, gdy żadna nie jest ustawiona – żaden origin nie jest dopuszczany (fail-closed).
Zachowanie:
Access-Control-Allow-Originjest ustawiane wyłącznie przy dokładnym dopasowaniu nagłówkaOrigindo listy; w przeciwnym razie nagłówek nie jest wysyłany w ogóle.- Dla dopasowanych originów zwracane jest również
Access-Control-Allow-Credentials: true. - Odpowiedzi zawsze zawierają
Vary: Origin. - Żądania preflight (
OPTIONS) kończą się statusem204bez treści. - Dozwolone metody:
GET, POST, PUT, DELETE, OPTIONS. Dozwolone nagłówki:Origin, X-Requested-With, Content-Type, Accept, Authorization, X-API-Key.
CORS ma znaczenie tylko wtedy, gdy wywołujesz API z przeglądarki (frontendu) z innej domeny. Dla integracji server-to-server (backend, cron, n8n, Make, Zapier itp.) CORS w praktyce nie ma znaczenia – żądania bez nagłówka Origin nie są przez niego ograniczane.
Jeśli integrujesz się z poziomu przeglądarki, skontaktuj się z nami w celu dodania Twojej domeny do listy dozwolonych originów.
Format odpowiedzi
Sukces
{
"success": true,
"data": { },
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
Sukces z paginacją
{
"success": true,
"data": [],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasMore": true
},
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
Błąd
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Opis błędu"
},
"timestamp": "2026-01-01T12:00:00.000Z"
}
Pole meta.timestamp (sukces) i timestamp (błąd) są obecne w każdej odpowiedzi. Niektóre endpointy dokładają do meta dodatkowe pola (np. note przy tworzeniu webhooka).
Walidacja parametrów
API rozróżnia dwie klasy parametrów.
1. Parametry zakresowe – przycinane po cichu. Wartość spoza zakresu nie powoduje błędu; jest sprowadzana do najbliższej dozwolonej.
| Parametr | Zachowanie przy wartości spoza zakresu |
|---|---|
page |
przycinane do zakresu 1–1000 (maksymalnie 1000 stron) |
limit (listings) |
przycinane do zakresu 1–100 |
limit (cities) |
przycinane do maks. 500 |
limit (deliveries) |
przycinane do maks. 100 |
days (analytics) |
przycinane do zakresu 1–30 |
2. Filtry – walidowane, zwracają VALIDATION_ERROR (400). Nieprawidłowa wartość filtra nigdy nie jest po cichu pomijana, ponieważ pominięcie zwróciłoby szerszy zbiór danych niż zamówiony.
| Parametr | Reguła |
|---|---|
position |
każda wartość musi być poprawnym UUID |
city |
musi być poprawnym UUID |
work_mode |
wyłącznie on-site, hybrid, remote |
experience |
wyłącznie 0-2, 2-5, 5+ |
contract_types |
wyłącznie kody z GET /contract-types (niewrażliwe na wielkość liter) |
sort |
wyłącznie newest, oldest, salary_high, salary_low |
salary_min, salary_max |
nieujemne liczby; salary_min nie może przekraczać salary_max |
| filtry wielowartościowe | maksymalnie 25 wartości na filtr |
Nieprawidłowy UUID w ścieżce (np. /listings/:id) zwraca INVALID_ID (400).
Pusty parametr = brak parametru.
?city=,?work_mode=czy?sort=(a także wartości złożone z samych spacji) są traktowane jak nieprzekazanie filtra, a nie jak błąd. Dzięki temu można bezpiecznie budować query z pustych pól formularza.
Parametry wielowartościowe
Filtry position, work_mode, experience i contract_types przyjmują wiele wartości w obu konwencjach – powtórzony parametr i lista rozdzielona przecinkami. Obie formy są równoważne:
# Równoważne zapisy
?experience=2-5&experience=5%2B
?experience=2-5,5%2B
Duplikaty i nadmiarowe białe znaki są usuwane automatycznie.
⚠️ Pamiętaj o URL-enkodowaniu wartości zawierających znaki specjalne –
5+musi zostać przesłane jako5%2B, w przeciwnym razie+zostanie zdekodowany jako spacja i wartość nie przejdzie walidacji.
Endpoints
Listings
GET /listings
Pobiera listę aktywnych ogłoszeń kandydatów. Zwracane są wyłącznie ogłoszenia w statusie active.
Wymaga uprawnienia: listings.read
Parametry query:
| Parametr | Typ | Wiele wartości | Opis | Przykład |
|---|---|---|---|---|
page |
int | – | Numer strony (default: 1, max: 1000) | ?page=2 |
limit |
int | – | Wyników na stronę (1-100, default: 20) | ?limit=50 |
position |
UUID | ✅ | ID stanowiska | ?position=uuid1,uuid2 |
city |
UUID | – | ID miasta (tylko jedna wartość) | ?city=uuid |
work_mode |
string | ✅ | Tryb pracy: on-site, hybrid, remote |
?work_mode=remote,hybrid |
experience |
string | ✅ | Doświadczenie: 0-2, 2-5, 5+ |
?experience=2-5,5%2B |
salary_min |
int | – | Dolna granica oczekiwanego wynagrodzenia | ?salary_min=5000 |
salary_max |
int | – | Górna granica oczekiwanego wynagrodzenia | ?salary_max=15000 |
contract_types |
string | ✅ | Kody rodzajów umów (niewrażliwe na wielkość liter) | ?contract_types=B2B,UOP |
sort |
string | – | Sortowanie: newest (default), oldest, salary_high, salary_low |
?sort=salary_high |
search |
string | – | Wyszukiwanie tekstowe w tytule i opisie (max 100 znaków) | ?search=python |
Kolumna „Wiele wartości" oznacza filtry przyjmujące zarówno listę po przecinku, jak i powtórzony parametr (maks. 25 wartości na filtr).
Uwagi:
salary_min/salary_maxfiltrują polesalary_expectation(oczekiwania kandydata), nie widełki oferty.searchjest sanityzowane – dozwolone są litery (w tym polskie), cyfry, spacje i myślnik; pozostałe znaki są zamieniane na spacje.contract_typesprzyjmuje kody ze słownikaGET /contract-typesw dowolnej wielkości liter (praktyka=PRAKTYKA=Praktyka). Kod spoza słownika zwracaVALIDATION_ERROR.- Sortowanie
newestużywarefreshed_at(odświeżenie ogłoszenia przez kandydata), a dopiero potemcreated_at.
Przykład request:
curl -X GET "https://zatrudnijmnie.pl/api/v1/listings?work_mode=remote&limit=10" \
-H "Authorization: Bearer zm_live_YOUR_API_KEY"
Przykład response:
{
"success": true,
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Senior Python Developer szuka pracy zdalnej",
"jobTitle": "Senior Python Developer szuka pracy zdalnej",
"position": {
"id": "3f1a0c22-1111-4a2b-8c3d-0e1f2a3b4c5d",
"name": "Python Developer",
"slug": "python-developer"
},
"city": {
"id": "7b2c9d10-2222-4e5f-9a1b-2c3d4e5f6a7b",
"name": "Warszawa",
"voivodeship": "Mazowieckie",
"slug": "warszawa"
},
"workMode": "remote",
"workTime": "full_time",
"experienceYears": "5+",
"salaryExpectation": 25000,
"salaryType": "brutto",
"contractTypes": ["B2B", "UOP"],
"createdAt": "2026-01-01T10:00:00.000Z",
"refreshedAt": "2026-01-01T10:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 150,
"totalPages": 15,
"hasMore": true
},
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
titleijobTitlezawierają tę samą wartość –jobTitleistnieje wyłącznie dla kompatybilności wstecznej.positionicitymogą byćnull, jeśli kandydat ich nie wskazał. Lista nigdy nie zawiera danych kontaktowych ani opisu ogłoszenia – te są dostępne wyłącznie w endpoincie szczegółów.
GET /listings/:id
Pobiera szczegóły ogłoszenia. Dane kontaktowe kandydata (contact) i cvUrl są dołączane tylko, gdy klucz API ma uprawnienie contacts.read.
Wymaga uprawnienia: listings.read (dla pól contact / cvUrl dodatkowo contacts.read)
Parametry path:
id– UUID ogłoszenia
Przykład request:
curl -X GET "https://zatrudnijmnie.pl/api/v1/listings/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer zm_live_YOUR_API_KEY"
Przykład response:
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Senior Python Developer szuka pracy zdalnej",
"jobTitle": "Senior Python Developer szuka pracy zdalnej",
"description": "Doświadczony programista Python z 8-letnim stażem...",
"position": {
"id": "3f1a0c22-1111-4a2b-8c3d-0e1f2a3b4c5d",
"name": "Python Developer",
"slug": "python-developer"
},
"city": {
"id": "7b2c9d10-2222-4e5f-9a1b-2c3d4e5f6a7b",
"name": "Warszawa",
"voivodeship": "Mazowieckie",
"slug": "warszawa"
},
"workMode": "remote",
"workTime": "full_time",
"experienceYears": "5+",
"salaryExpectation": 25000,
"salaryType": "brutto",
"contractTypes": ["B2B", "UOP"],
"educationLevel": "master",
"languages": [
{"language": "Angielski", "level": "C1"},
{"language": "Niemiecki", "level": "B2"}
],
"contact": {
"fullName": "Jan Kowalski",
"email": "[email protected]",
"phone": "123456789"
},
"cvUrl": "/api/v1/listings/550e8400-e29b-41d4-a716-446655440000/cv",
"createdAt": "2026-01-01T10:00:00.000Z",
"refreshedAt": "2026-01-01T10:00:00.000Z"
},
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
Dane kontaktowe (
contact) oraz CV (cvUrl) są zwracane wyłącznie, gdy klucz API ma uprawnieniecontacts.read. Klucze bez tego uprawnienia otrzymują szczegóły ogłoszenia bez pólcontacticvUrl– dane osobowe nie są wtedy nawet pobierane z bazy (minimalizacja danych, RODO Art. 5(1)(c)).Gdy klucz ma uprawnienie
contacts.read, obiektcontact(imię i nazwisko, email, telefon) jest zawsze obecny – pola te są wymagane w każdym ogłoszeniu.CV (
cvUrl) jest dodatkowo opcjonalne – pole pojawia się tylko gdy kandydat dołączył CV do ogłoszenia. URL wskazuje na endpoint API do pobrania CV.
⚠️ Uwaga: Każde ujawnienie danych kontaktowych i CV jest rejestrowane w dedykowanym logu audytowym zgodnie z RODO (Art. 30) – zapisywany jest klucz API, organizacja, ogłoszenie i identyfikator kandydata.
GET /listings/:id/cv
Pobiera CV kandydata. Zwraca tymczasowy (presigned) URL do pobrania pliku.
Wymaga uprawnienia: contacts.read
Parametry path:
id– UUID ogłoszenia
CV jest opcjonalne – nie każde ogłoszenie je zawiera. Sprawdź obecność pola
cvUrlw odpowiedziGET /listings/:id.
Przykład request:
curl -X GET "https://zatrudnijmnie.pl/api/v1/listings/550e8400-e29b-41d4-a716-446655440000/cv" \
-H "Authorization: Bearer zm_live_YOUR_API_KEY"
Przykład response (CV dostępne):
{
"success": true,
"data": {
"downloadUrl": "https://storage.example.com/cv/signed-url?token=...",
"filename": "Jan_Kowalski_CV.pdf"
},
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
Przykład response (brak CV):
{
"success": false,
"error": {
"code": "CV_NOT_AVAILABLE",
"message": "To ogłoszenie nie zawiera CV"
},
"timestamp": "2026-01-01T12:00:00.000Z"
}
⚠️ Presigned URL jest ważny przez krótki czas (domyślnie 60 sekund, konfigurowalne w zakresie 10–600 s). Pobierz plik niezwłocznie po otrzymaniu URL – nie zapisuj tego linku w swojej bazie.
Analytics
GET /analytics
Pobiera zagregowane statystyki użycia API dla całej organizacji.
Wymaga uprawnienia: analytics.read
Parametry query:
| Parametr | Typ | Opis | Default |
|---|---|---|---|
days |
int | Okres w dniach (1-30) | 30 |
Przykład response:
{
"success": true,
"data": {
"period": {
"days": 30,
"from": "2025-12-02T12:00:00.000Z",
"to": "2026-01-01T12:00:00.000Z"
},
"requests": {
"total": 15420,
"successful": 15380,
"failed": 40,
"successRate": "99.74%"
},
"performance": {
"avgResponseTimeMs": 127.5
},
"breakdown": {
"byEndpoint": {
"/api/v1/listings": 12000,
"/api/v1/listings/:id": 3420
},
"byDay": [
{"date": "2025-12-31", "count": 520},
{"date": "2026-01-01", "count": 480}
]
}
},
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
successRateierrorRatesą zwracane jako stringi z symbolem procenta (np."99.74%"), a nie jako liczby.
GET /analytics/usage
Pobiera statystyki użycia w rozbiciu na poszczególne klucze API organizacji. Zwracane są wszystkie klucze zespołu (aktywne i dezaktywowane), niezależnie od tego, którym kluczem wykonano żądanie.
Wymaga uprawnienia: analytics.read
Przykład response:
{
"success": true,
"data": {
"team": {
"id": "9c3f1b44-3333-4a5b-8c6d-7e8f9a0b1c2d",
"name": "Firma Rekrutacyjna XYZ",
"plan": "team_premium"
},
"keys": [
{
"keyId": "a1b2c3d4-4444-4e5f-9a0b-1c2d3e4f5a6b",
"keyName": "Production Key",
"keyPrefix": "abc12345",
"isActive": true,
"totalRequests": 15420,
"totalErrors": 40,
"errorRate": "0.26%",
"lastUsedAt": "2026-01-01T12:00:00.000Z",
"createdAt": "2025-06-01T00:00:00.000Z"
}
],
"totals": {
"activeKeys": 3,
"totalKeys": 5,
"totalRequests": 45000,
"totalErrors": 120
}
},
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
keyPrefixto 8 znaków po przedrostkuzm_live_– służy do rozpoznania klucza w panelu.planprzyjmuje wartościteam_basic,team_premium,team_premium_pluslubteam_enterprise. LicznikitotalRequests/totalErrorssą narastające od utworzenia klucza (nie podlegają parametrowidays).
Dictionaries
Endpointy słownikowe wymagają jedynie ważnego klucza API – nie sprawdzają uprawnień.
GET /positions
Pobiera słownik stanowisk. Struktura jest dwupoziomowa: rekordy z parent_id: null to kategorie, pozostałe to stanowiska w ramach kategorii.
{
"success": true,
"data": [
{
"id": "1a2b3c4d-5555-4a6b-8c7d-9e0f1a2b3c4d",
"name": "IT / Programowanie",
"slug": "it-programowanie",
"parent_id": null,
"display_order": 1
},
{
"id": "3f1a0c22-1111-4a2b-8c3d-0e1f2a3b4c5d",
"name": "Python Developer",
"slug": "python-developer",
"parent_id": "1a2b3c4d-5555-4a6b-8c7d-9e0f1a2b3c4d",
"display_order": 12
}
],
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
Odpowiedź używa
snake_case(parent_id,display_order), ponieważ zwraca surowe rekordy słownika. Wyniki są cache'owane po stronie serwera – zmiany w słowniku mogą być widoczne z opóźnieniem.
GET /cities
Pobiera słownik miast (tylko aktywne), posortowany alfabetycznie.
Parametry query:
| Parametr | Typ | Opis |
|---|---|---|
query |
string | Filtr po nazwie – dopasowanie od początku nazwy, min. 2 znaki (krótsze są ignorowane) |
limit |
int | Max wyników (default: 100, max: 500) |
curl -X GET "https://zatrudnijmnie.pl/api/v1/cities?query=War" \
-H "Authorization: Bearer zm_live_YOUR_API_KEY"
{
"success": true,
"data": [
{
"id": "7b2c9d10-2222-4e5f-9a1b-2c3d4e5f6a7b",
"name": "Warszawa",
"slug": "warszawa",
"voivodeship": "Mazowieckie"
}
],
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
GET /contract-types
Pobiera słownik rodzajów umów (tylko aktywne). Kody z pola code są wartościami przyjmowanymi przez filtr contract_types w GET /listings.
{
"success": true,
"data": [
{"code": "UOP", "name": "Umowa o pracę"},
{"code": "B2B", "name": "Kontrakt B2B"},
{"code": "UZ", "name": "Umowa zlecenie"},
{"code": "UOD", "name": "Umowa o dzieło"},
{"code": "Praktyka", "name": "Praktyka / Staż"}
],
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
Zwróć uwagę, że kod
Praktykanie jest zapisany wielkimi literami. Filtrcontract_typesdopasowuje kody niezależnie od wielkości liter, ale w polucontractTypesodpowiedzi zwracana jest kanoniczna forma ze słownika. Nie zakładaj na sztywno listy pięciu kodów – pobieraj ją z tego endpointu.
Webhooks
Wszystkie endpointy webhooków wymagają uprawnienia webhooks.manage oraz planu Team Premium Plus lub Team Enterprise.
GET /webhooks
Pobiera listę skonfigurowanych webhooków organizacji, od najnowszego.
Wymaga uprawnienia: webhooks.manage
{
"success": true,
"data": [
{
"id": "b5c6d7e8-6666-4f0a-9b1c-2d3e4f5a6b7c",
"name": "New Candidates Notification",
"url": "https://your-system.com/webhook",
"events": ["listing.created", "listing.updated"],
"isActive": true,
"totalSent": 1284,
"totalFailed": 3,
"lastTriggeredAt": "2026-01-01T11:59:00.000Z",
"lastSuccessAt": "2026-01-01T11:59:00.000Z",
"lastError": null,
"createdAt": "2025-06-01T00:00:00.000Z",
"webhook_name": "New Candidates Notification",
"webhook_url": "https://your-system.com/webhook",
"is_active": true,
"total_sent": 1284,
"total_failed": 3,
"last_triggered_at": "2026-01-01T11:59:00.000Z",
"last_success_at": "2026-01-01T11:59:00.000Z",
"last_error": null,
"created_at": "2025-06-01T00:00:00.000Z"
}
],
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z",
"deprecation": "Pola snake_case (webhook_name, webhook_url, is_active, ...) są przestarzałe i zostaną usunięte w API v2. Używaj pól camelCase."
}
}
⚠️ Pola
snake_casesą przestarzałe (deprecated). Endpoint zwracał je historycznie, w odróżnieniu od reszty API używającejcamelCase. Obie formy niosą tę samą wartość i są zwracane równolegle, aby nie zepsuć istniejących integracji. Nowy kod powinien czytać wyłącznie polacamelCase– aliasysnake_casezostaną usunięte w API v2. Kształt obiektu jest teraz identyczny jak w odpowiedziPOST /webhooks.Sekret webhooka nigdy nie jest zwracany przez ten endpoint.
POST /webhooks
Tworzy nowy webhook.
Wymaga uprawnienia: webhooks.manage
Body:
{
"name": "New Candidates Notification",
"url": "https://your-system.com/webhook",
"events": ["listing.created", "listing.updated"]
}
Walidacja:
| Pole | Reguły |
|---|---|
name |
wymagane, string, 1–100 znaków (po przycięciu białych znaków) |
url |
wymagane, string, max 500 znaków (po przycięciu białych znaków), musi zaczynać się od https:// |
events |
opcjonalne; jeśli podane, musi być tablicą niepustą; pominięcie = subskrypcja wszystkich dostępnych eventów |
Liczba webhooków na organizację jest ograniczona (domyślnie 10). Przekroczenie limitu zwraca 409 z kodem WEBHOOK_LIMIT_REACHED.
Adres URL przechodzi kontrolę SSRF: odrzucane są localhost, 0.0.0.0, domeny .local i .internal, adresy prywatne/loopback/link-local (IPv4 i IPv6, także w formie mapowanej), URL-e z osadzonymi danymi logowania oraz hosty, których nie da się rozwiązać w DNS. Kontrola jest powtarzana przy każdej wysyłce, a zweryfikowany publiczny adres IP jest przypinany do faktycznego połączenia, aby zablokować DNS rebinding.
Dostępne eventy:
listing.created– publikacja nowego ogłoszenialisting.updated– aktualizacja ogłoszenialisting.deleted– usunięcie ogłoszenia (patrz uwaga niżej)
Response zawiera secret (tylko raz!):
{
"success": true,
"data": {
"id": "b5c6d7e8-6666-4f0a-9b1c-2d3e4f5a6b7c",
"name": "New Candidates Notification",
"url": "https://your-system.com/webhook",
"events": ["listing.created", "listing.updated"],
"isActive": true,
"totalSent": 0,
"totalFailed": 0,
"lastTriggeredAt": null,
"lastSuccessAt": null,
"lastError": null,
"createdAt": "2026-01-01T12:00:00.000Z",
"secret": "abc123...xyz789"
},
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z",
"note": "Zapisz secret - nie będzie można go ponownie wyświetlić"
}
}
secret to 64 znaki szesnastkowe. Zapisz go bezpiecznie – jest niezbędny do weryfikacji sygnatury i nie da się go później odczytać (w bazie trzymany jest wyłącznie jego skrót).
DELETE /webhooks/:id
Usuwa webhook należący do Twojej organizacji.
Wymaga uprawnienia: webhooks.manage
{
"success": true,
"data": { "deleted": true },
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
Gdy webhook o podanym ID nie istnieje albo nie należy do Twojej organizacji, endpoint zwraca
404 WEBHOOK_NOT_FOUND. Odpowiedź sukcesu potwierdza usunięcie właściwego webhooka.
POST /webhooks/:id/test
Wysyła testowy payload do webhooka.
Wymaga uprawnienia: webhooks.manage
Response:
{
"success": true,
"data": {
"message": "Testowy webhook został wysłany",
"webhookId": "b5c6d7e8-6666-4f0a-9b1c-2d3e4f5a6b7c"
},
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
Testowy request wysyła event
test.pingz payloadem:{ "test": true, "message": "To jest testowy webhook z ZatrudnijMnie", "timestamp": "2026-01-01T12:00:00.000Z" }Uwaga:
test.pingnie jest eventem subskrybowanym – jest wysyłany do wskazanego webhooka niezależnie od jego listyevents. Twój endpoint powinien go rozpoznawać i ignorować. Odpowiedźsuccess: trueoznacza, że endpoint odpowiedział kodem 2xx na pierwszą próbę. Błąd połączenia lub odpowiedź spoza 2xx zwraca błąd API; szczegóły próby sprawdzisz wGET /webhooks/:id/deliveries.
GET /webhooks/:id/deliveries
Zwraca historię wysyłek webhooka, od najnowszej.
Wymaga uprawnienia: webhooks.manage
Parametry query:
| Parametr | Typ | Opis | Default |
|---|---|---|---|
limit |
int | Max wyników (max: 100) | 50 |
Przykład response:
{
"success": true,
"data": [
{
"id": "c6d7e8f9-7777-4a1b-9c2d-3e4f5a6b7c8d",
"deliveryId": "d7e8f9a0-8888-4b2c-9d3e-4f5a6b7c8d9e",
"eventType": "listing.created",
"statusCode": 200,
"success": true,
"attemptNumber": 1,
"responseTimeMs": 142,
"errorMessage": null,
"createdAt": "2026-01-01T10:00:00.000Z"
}
],
"meta": {
"timestamp": "2026-01-01T12:00:00.000Z"
}
}
statusCode: 0oznacza, że połączenie nie doszło do skutku (timeout, błąd DNS, odrzucone TLS) – szczegóły znajdziesz werrorMessage. Każda próba (również ponowienie) tworzy osobny wpis z własnymattemptNumber.
Webhooki – format payloadu i weryfikacja sygnatury
Format payloadu
Każdy webhook POST zawiera JSON o stałej kopercie:
{
"id": "d7e8f9a0-8888-4b2c-9d3e-4f5a6b7c8d9e",
"event": "listing.created",
"created_at": "2026-01-01T10:00:00.000Z",
"data": { }
}
⚠️ Obiekt
datanie jest tym samym formatem, co odpowiedźGET /listings/:id. Jest to płaski obiekt wsnake_casez ograniczonym zestawem pól. Aby pobrać pełne dane ogłoszenia (w tym opis i dane kontaktowe), wykonajGET /listings/:idz użyciemdata.id.
data dla listing.created:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"job_title": "Senior Python Developer szuka pracy zdalnej",
"position_id": "3f1a0c22-1111-4a2b-8c3d-0e1f2a3b4c5d",
"city_id": "7b2c9d10-2222-4e5f-9a1b-2c3d4e5f6a7b",
"salary_expectation": 25000,
"salary_type": "brutto",
"experience_years": "5+",
"work_mode": "remote",
"work_time_preference": "full_time",
"created_at": "2026-01-01T10:00:00.000Z"
}
data dla listing.updated – identyczny zestaw pól, z updated_at zamiast created_at. Przy przywróceniu wcześniej usuniętego ogłoszenia pole czasu nazywa się restored_at.
data dla listing.deleted – identyczny zestaw pól, z deleted_at zamiast created_at.
Uwagi:
- Pola opcjonalne (
position_id,city_id,salary_expectation,work_mode,experience_years) mogą mieć wartośćnull. job_titleodpowiada polutitlew REST API.listing.deletedjest wysyłany zarówno przy usunięciu miękkim (z możliwością przywrócenia), jak i trwałym. Jeśli ogłoszenie zostało najpierw usunięte miękko, a później trwale, event wysyłany jest tylko raz – przy pierwszym usunięciu. Zdarzenie usunięcia nie niesie informacji, czy usunięcie jest odwracalne; jeśli ogłoszenie zostanie przywrócone, otrzymaszlisting.updated.
Nagłówki żądania webhook
Content-Type: application/json
User-Agent: ZatrudnijMnie-Webhooks/1.0
X-Webhook-Id: d7e8f9a0-8888-4b2c-9d3e-4f5a6b7c8d9e
X-Webhook-Event: listing.created
X-Webhook-Timestamp: 1767261600000
X-Webhook-Signature: t=1767261600000,v1=abc123...
X-Webhook-Timestamp jest podawany w milisekundach (Unix epoch). Ta sama wartość występuje w polu t= sygnatury.
Przekierowania HTTP nie są śledzone (
redirect: 'manual') – odpowiedź 3xx jest traktowana jak niepowodzenie. Twój endpoint musi odpowiadać bezpośrednio kodem 2xx.
Weryfikacja sygnatury
Sygnatura w nagłówku X-Webhook-Signature ma format t={timestamp},v1={hmac}, gdzie:
signedPayload = "{timestamp}.{rawBody}"
hmac = HMAC-SHA256(key = secret, message = signedPayload)
signature = "t={timestamp},v1={hmac}"
Sekret dostajesz tylko raz przy tworzeniu webhooka. Użyj go bezpośrednio jako klucza HMAC i przechowuj jak dane uwierzytelniające.
Weryfikuj sygnaturę na surowym ciele żądania (raw body), przed jakimkolwiek parsowaniem JSON – ponowna serializacja zmieni bajty i unieważni podpis.
Node.js:
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
if (
typeof rawBody !== 'string'
|| typeof signatureHeader !== 'string'
|| typeof secret !== 'string'
|| secret.length === 0
|| !Number.isFinite(toleranceSec)
|| toleranceSec < 0
) {
return false;
}
// Dokładny format zapobiega akceptacji zduplikowanych lub nadmiarowych pól.
const match = /^t=(\d+),v1=([0-9a-f]{64})$/i.exec(signatureHeader);
if (!match) return false;
const timestamp = Number(match[1]);
const received = match[2];
if (!Number.isSafeInteger(timestamp)) return false;
// Sprawdź tolerancję czasową (ochrona przed replay attacks).
// timestamp jest w milisekundach.
if (Math.abs(Date.now() - timestamp) > toleranceSec * 1000) {
return false;
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(received, 'hex'),
Buffer.from(expected, 'hex')
);
}
// Express: pozyskanie raw body
// app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
// const ok = verifyWebhook(req.body.toString('utf8'), req.get('X-Webhook-Signature'), SECRET);
// if (!ok) return res.sendStatus(401);
// res.sendStatus(200);
// });
Python:
import hashlib, hmac, math, re, time
def verify_webhook(raw_body: str, signature_header: str, secret: str, tolerance_sec: int = 300) -> bool:
if not isinstance(raw_body, str) or not isinstance(signature_header, str):
return False
if (
not isinstance(secret, str)
or not secret
or isinstance(tolerance_sec, bool)
or not isinstance(tolerance_sec, (int, float))
or not math.isfinite(tolerance_sec)
or tolerance_sec < 0
):
return False
match = re.fullmatch(r't=(\d+),v1=([0-9a-fA-F]{64})', signature_header)
if not match:
return False
timestamp = int(match.group(1))
received = match.group(2).lower()
# timestamp jest w milisekundach
if abs(time.time() * 1000 - timestamp) > tolerance_sec * 1000:
return False
expected = hmac.new(
secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(received, expected)
Polityka retry
Wysyłka jest uznana za nieudaną, gdy odpowiedź ma status inny niż 2xx (w tym 3xx), połączenie się nie powiedzie lub przekroczy 10 sekund. Ponawiane są błędy sieciowe, timeouty, statusy 408, 425, 429 i 5xx. Pozostałe 3xx/4xx są trwałą odmową i nie są ponawiane.
| Próba | Opóźnienie od poprzedniej |
|---|---|
| 1 (oryginalna) | – |
| 2 | 1 minuta |
| 3 | 5 minut |
| 4 | 15 minut |
Domyślnie wykonywane są 4 próby łącznie (max_retries = 3). Po wyczerpaniu prób wysyłka jest porzucana – każda próba trafia do historii dostarczeń, a licznik total_failed webhooka rośnie. Webhook nie jest automatycznie dezaktywowany.
Przed każdym ponowieniem sprawdzamy aktualny stan webhooka: jeśli w międzyczasie został wyłączony lub usunięty, zaplanowane ponowienia nie są wysyłane.
Zdarzenia i terminy ponowień są przechowywane w trwałym outboxie bazy danych. Restart lub przeładowanie procesu nie usuwa oczekującej wysyłki. Model dostarczania to co najmniej raz: awaria procesu po wysłaniu, ale przed zapisaniem potwierdzenia może spowodować ponowne dostarczenie tego samego id. Chwilowe błędy DNS są traktowane jak błędy sieciowe i podlegają retry; adres rozwiązujący się do sieci prywatnej jest trwale blokowany.
Wskazówki dla odbiorcy:
- Odpowiadaj
2xxnatychmiast po przyjęciu zdarzenia, a przetwarzanie wykonuj asynchronicznie – przekroczenie 10 s uruchamia ponowienie. - Zapewnij idempotencję na podstawie
X-Webhook-Id/idz payloadu: ponowienie ma ten samiddostarczenia, więc to samo zdarzenie może dotrzeć wielokrotnie.
Max rozmiar payloadu: 64 KB. Zdarzenia przekraczające ten limit nie są wysyłane.
Słowniki wartości
Wartości enumeracyjne występujące w odpowiedziach i filtrach:
| Pole (REST) | Pole (webhook) | Dozwolone wartości |
|---|---|---|
workMode |
work_mode |
on-site, hybrid, remote, null |
workTime |
work_time_preference |
full_time, part_time, temporary |
experienceYears |
experience_years |
0-2, 2-5, 5+, null |
salaryType |
salary_type |
brutto, netto |
educationLevel |
– | primary, secondary, bachelor, master, phd, null |
contractTypes |
– | kody z GET /contract-types |
languages[].level |
– | A1, A2, B1, B2, C1, C2, Native |
Plany organizacji (data.team.plan w /analytics/usage): team_basic, team_premium, team_premium_plus, team_enterprise.
Kody błędów
| Kod | HTTP | Opis |
|---|---|---|
API_KEY_MISSING |
401 | Brak klucza API w nagłówku (lub próba przekazania go w query) |
API_KEY_INVALID |
401 | Nieprawidłowy, nieaktywny lub wygasły klucz; wygasły/zawieszony dostęp organizacji; plan bez API |
PERMISSION_DENIED |
403 | Klucz nie ma wymaganego uprawnienia (lub plan nie obejmuje webhooków) |
NOT_FOUND |
404 | Zasób nie znaleziony (ogłoszenie nieistniejące lub nieaktywne) |
WEBHOOK_NOT_FOUND |
404 | Webhook nie istnieje lub należy do innej organizacji |
CV_NOT_AVAILABLE |
404 | Ogłoszenie nie zawiera CV lub CV jest niedostępne |
INVALID_ID |
400 | Nieprawidłowy format UUID w ścieżce |
VALIDATION_ERROR |
400 | Błąd walidacji treści żądania (POST /webhooks) |
WEBHOOK_ACCESS_DENIED |
403 | Webhook lub dostęp organizacji nie jest aktywny albo plan nie obejmuje webhooków |
WEBHOOK_LIMIT_REACHED |
409 | Osiągnięto limit webhooków dla organizacji |
WEBHOOK_DELIVERY_FAILED |
400 lub 502 | Testowy webhook ma niebezpieczny URL albo endpoint nie odpowiedział kodem 2xx |
CONTACT_QUOTA_EXCEEDED |
402 | Wyczerpano limit pobrań danych kontaktowych w oknie 30 dni |
CONTACT_QUOTA_UNAVAILABLE |
503 | Nie udało się atomowo zweryfikować i zapisać pobrania kontaktu |
RATE_LIMIT_EXCEEDED |
429 | Przekroczono godzinowy lub dzienny limit requestów |
CV_ERROR |
500 | Błąd generowania linku do CV |
INTERNAL_ERROR |
500 | Wewnętrzny błąd serwera |
Zalecana obsługa po stronie klienta: 401/403 – nie ponawiaj, popraw konfigurację klucza; 402 – nie ponawiaj, dokup pakiet pobrań lub poczekaj do daty z pola resetAt; 429 – ponów po czasie z nagłówka Retry-After; 5xx – ponów z wykładniczym backoffem.
Przykłady
cURL
# Lista ogłoszeń zdalnych
curl -X GET "https://zatrudnijmnie.pl/api/v1/listings?work_mode=remote&limit=10" \
-H "Authorization: Bearer zm_live_YOUR_API_KEY"
# Kilka wartości jednego filtra - powtórzony parametr, nie przecinek
curl -G "https://zatrudnijmnie.pl/api/v1/listings" \
--data-urlencode "experience=2-5" \
--data-urlencode "experience=5+" \
-H "Authorization: Bearer zm_live_YOUR_API_KEY"
# Szczegóły kandydata
curl -X GET "https://zatrudnijmnie.pl/api/v1/listings/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer zm_live_YOUR_API_KEY"
JavaScript (fetch)
const API_KEY = process.env.ZATRUDNIJMNIE_API_KEY;
const BASE_URL = 'https://zatrudnijmnie.pl/api/v1';
async function getListings(filters = {}) {
// URLSearchParams obsługuje wiele wartości tego samego klucza,
// co jest wymaganą formą dla position / work_mode / experience
const params = new URLSearchParams();
for (const [key, value] of Object.entries(filters)) {
if (Array.isArray(value)) {
value.forEach(v => params.append(key, v));
} else {
params.append(key, value);
}
}
const response = await fetch(`${BASE_URL}/listings?${params}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get('Retry-After') || 60);
throw new Error(`Rate limit - ponów za ${retryAfter}s`);
}
const body = await response.json();
if (!body.success) {
throw new Error(`${body.error.code}: ${body.error.message}`);
}
return body;
}
// Użycie
const candidates = await getListings({
work_mode: 'remote',
experience: ['2-5', '5+'],
limit: 50
});
Python (requests)
import os
import requests
API_KEY = os.environ['ZATRUDNIJMNIE_API_KEY']
BASE_URL = 'https://zatrudnijmnie.pl/api/v1'
headers = {'Authorization': f'Bearer {API_KEY}'}
# Lista ogłoszeń - lista w params daje powtórzony parametr
response = requests.get(
f'{BASE_URL}/listings',
headers=headers,
params={
'work_mode': 'remote',
'experience': ['2-5', '5+'],
'limit': 50
}
)
response.raise_for_status()
candidates = response.json()
# Szczegóły kandydata (wymaga contacts.read dla pola 'contact')
listing_id = candidates['data'][0]['id']
detail = requests.get(f'{BASE_URL}/listings/{listing_id}', headers=headers).json()
contact = detail['data'].get('contact')
if contact:
print(f"Kontakt: {contact['email']}")
else:
print("Klucz API nie ma uprawnienia contacts.read")
PHP (Guzzle)
<?php
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://zatrudnijmnie.pl/api/v1/',
'headers' => [
'Authorization' => 'Bearer ' . getenv('ZATRUDNIJMNIE_API_KEY')
]
]);
// Lista ogłoszeń
$response = $client->get('listings', [
'query' => [
'work_mode' => 'remote',
'limit' => 50
]
]);
$candidates = json_decode($response->getBody(), true);
Wsparcie
- 📧 Email: [email protected]
- 📖 Dokumentacja: https://zatrudnijmnie.pl/docs/api
Ostatnia aktualizacja: 31 sierpnia 2026