Dostęp do API wymaga planu Premium lub wyższego. Zobacz plany zespołowe →
ZatrudnijMnie API Documentation
Version: 1.0
Base URL: https://zatrudnijmnie.pl/api/v1
Authentication: API Key (Bearer token)
Spis treści
Wprowadzenie
API ZatrudnijMnie umożliwia programowy dostęp do ogłoszeń kandydatów szukających pracy. Dostępne jest dla Kont Zespołowych z planami Team Premium i wyżej.
Dostępność API według planów
| Plan | API Access | Limit kluczy | Webhooki |
|---|---|---|---|
| Team Basic | ❌ | - | ❌ |
| Team Premium | ✅ | 5 | ❌ |
| Team Premium Plus | ✅ | 10 | ❌ |
| Team Enterprise | ✅ | 50 | ✅ |
Rate limit jest konfigurowany per klucz (domyślnie 1 000 req/h, max 10 000 req/h i 100 000 req/dzień). Webhooki (
webhooks.manage) są dostępne wyłącznie dla planu Team Enterprise (wymuszane server-side).
Autoryzacja
API wymaga klucza API przekazywanego w nagłówku HTTP.
Metody autoryzacji
1. Bearer Token (zalecane):
Authorization: Bearer zm_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2. X-Api-Key Header:
X-Api-Key: zm_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
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ć!
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
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:
rate_limit_per_hour: 1 000 req/h (max: 10 000)rate_limit_per_day: 10 000 req/dzień (max: 100 000)
Nagłówki odpowiedzi
X-RateLimit-Limit-Hour: 2000
X-RateLimit-Limit-Day: 20000
X-RateLimit-Remaining-Hour: 1995
X-RateLimit-Remaining-Day: 19990
Przekroczenie limitu
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Przekroczono limit requestów. Spróbuj ponownie za 3600 sekund.",
"retryAfter": 3600
}
}
Nagłówek Retry-After wskazuje 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.
- Domyślnie
Access-Control-Allow-Originjest ustawione na*(kompatybilność). - Jeśli ustawisz zmienną środowiskową
API_V1_CORS_ORIGINS(lista originów rozdzielona przecinkami), to API będzie dopuszczać tylko te originy.To jest opcjonalna konfiguracja CORS tylko dla publicznego API (/api/v1), czyli endpointów używanych przez integracje z kluczem API.
CORS ma znaczenie tylko wtedy, gdy ktoś wywołuje Twoje 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.
Format odpowiedzi
Sukces
{
"success": true,
"data": { ... },
"meta": {
"timestamp": "2025-01-01T12:00:00.000Z"
}
}
Sukces z paginacją
{
"success": true,
"data": [ ... ],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasMore": true
},
"meta": {
"timestamp": "2025-01-01T12:00:00.000Z"
}
}
Błąd
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Opis błędu"
},
"timestamp": "2025-01-01T12:00:00.000Z"
}
Endpoints
Listings
GET /listings
Pobiera listę ogłoszeń kandydatów.
Parametry query:
| Parametr | Typ | Opis | Przykład |
|---|---|---|---|
page |
int | Numer strony (default: 1) | ?page=2 |
limit |
int | Wyników na stronę (1-100, default: 20) | ?limit=50 |
position |
string/array | ID stanowiska | ?position=uuid1,uuid2 |
city |
string | ID miasta | ?city=uuid |
work_mode |
string/array | Tryb pracy: on-site, hybrid, remote | ?work_mode=remote |
experience |
string/array | Doświadczenie: 0-2, 2-5, 5+ | ?experience=2-5,5+ |
salary_min |
int | Oczekiwane wynagrodzenie (dolna granica) | ?salary_min=5000 |
salary_max |
int | Maksymalne wynagrodzenie | ?salary_max=15000 |
contract_types |
string/array | Rodzaje umów: B2B, UOP, UZ, UOD | ?contract_types=B2B,UOP |
sort |
string | Sortowanie: newest, oldest, salary_high, salary_low | ?sort=salary_high |
search |
string | Wyszukiwanie tekstowe (max 100 znaków) | ?search=python |
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",
"position": {
"id": "...",
"name": "Python Developer",
"slug": "python-developer"
},
"city": {
"id": "...",
"name": "Warszawa",
"voivodeship": "Mazowieckie",
"slug": "warszawa"
},
"workMode": "remote",
"workTime": "full_time",
"experienceYears": "5+",
"salaryExpectation": 25000,
"salaryType": "brutto",
"contractTypes": ["B2B", "UOP"],
"createdAt": "2025-01-01T10:00:00.000Z",
"refreshedAt": "2025-01-01T10:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 150,
"totalPages": 15,
"hasMore": true
}
}
GET /listings/:id
Pobiera szczegóły ogłoszenia z danymi kontaktowymi kandydata.
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",
"description": "Doświadczony programista Python z 8-letnim stażem...",
"position": { ... },
"city": { ... },
"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": "2025-01-01T10:00:00.000Z",
"refreshedAt": "2025-01-01T10:00:00.000Z"
}
}
Dane kontaktowe (
contact) są zawsze obecne — imię i nazwisko, email oraz telefon są wymagane w każdym ogłoszeniu.CV (
cvUrl) jest opcjonalne — pole pojawia się tylko gdy kandydat dołączył CV do ogłoszenia. URL wskazuje na endpoint API do pobrania CV.
⚠️ Uwaga: Dostęp do danych kontaktowych i CV jest logowany zgodnie z RODO (Art. 30).
GET /listings/:id/cv
Pobiera CV kandydata. Zwraca tymczasowy (presigned) URL do pobrania pliku.
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"
}
}
Przykład response (brak CV):
{
"success": false,
"error": {
"code": "CV_NOT_AVAILABLE",
"message": "To ogłoszenie nie zawiera CV"
}
}
⚠️ Presigned URL jest ważny przez krótki czas (konfigurowalny, domyślnie 60 sekund). Pobierz plik niezwłocznie po otrzymaniu URL.
Analytics
GET /analytics
Pobiera statystyki użycia API.
Parametry query:
| Parametr | Typ | Opis | Default |
|---|---|---|---|
days |
int | Okres w dniach (1-30) | 30 |
Przykład response:
{
"success": true,
"data": {
"period": {
"days": 30,
"from": "2024-12-01T00:00:00.000Z",
"to": "2025-01-01T00: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": "2024-12-31", "count": 520},
{"date": "2025-01-01", "count": 480}
]
}
}
}
GET /analytics/usage
Pobiera szczegółowe statystyki użycia kluczy API.
Przykład response:
{
"success": true,
"data": {
"team": {
"id": "...",
"name": "Firma Rekrutacyjna XYZ",
"plan": "team_premium"
},
"keys": [
{
"keyId": "...",
"keyName": "Production Key",
"keyPrefix": "zm_live_abc1",
"isActive": true,
"totalRequests": 15420,
"totalErrors": 40,
"errorRate": "0.26%",
"lastUsedAt": "2025-01-01T12:00:00.000Z",
"createdAt": "2024-06-01T00:00:00.000Z"
}
],
"totals": {
"activeKeys": 3,
"totalKeys": 5,
"totalRequests": 45000,
"totalErrors": 120
}
}
}
Dictionaries
GET /positions
Pobiera słownik stanowisk.
{
"success": true,
"data": [
{
"id": "...",
"name": "IT / Programowanie",
"slug": "it-programowanie",
"parent_id": null
},
{
"id": "...",
"name": "Python Developer",
"slug": "python-developer",
"parent_id": "..."
}
]
}
GET /cities
Pobiera słownik miast.
Parametry query:
| Parametr | Typ | Opis |
|---|---|---|
query |
string | Filtr po nazwie (min. 2 znaki) |
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"
GET /contract-types
Pobiera słownik rodzajów umów.
{
"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"}
]
}
Webhooks
GET /webhooks
Pobiera listę skonfigurowanych webhooków.
Wymaga uprawnienia: webhooks.manage
POST /webhooks
Tworzy nowy webhook.
Body:
{
"name": "New Candidates Notification",
"url": "https://your-system.com/webhook",
"events": ["listing.created", "listing.updated"]
}
Dostępne eventy:
listing.created— nowe ogłoszenielisting.updated— aktualizacja ogłoszenialisting.deleted— usunięcie ogłoszenia
⚠️ Webhooki wymagają planu Team Enterprise.
Response zawiera secret (tylko raz!):
{
"success": true,
"data": {
"id": "...",
"name": "New Candidates Notification",
"url": "https://your-system.com/webhook",
"events": ["listing.created", "listing.updated"],
"secret": "abc123...xyz789"
},
"meta": {
"note": "Zapisz secret - nie będzie można go ponownie wyświetlić"
}
}
DELETE /webhooks/:id
Usuwa webhook.
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": "..."
}
}
Testowy request wysyła event
test.pingz payloadem{ test: true, ... }.
GET /webhooks/:id/deliveries
Zwraca historię wysyłek webhooka (max 100 ostatnich).
Wymaga uprawnienia: webhooks.manage
Parametry query:
| Parametr | Typ | Opis | Default |
|---|---|---|---|
limit |
int | Max wyników (1-100) | 50 |
Przykład response:
{
"success": true,
"data": [
{
"id": "...",
"deliveryId": "uuid",
"eventType": "listing.created",
"statusCode": 200,
"success": true,
"attemptNumber": 1,
"responseTimeMs": 142,
"errorMessage": null,
"createdAt": "2026-01-01T10:00:00.000Z"
}
]
}
Webhooki — format payloadu i weryfikacja sygnatury
Format payloadu
Każdy webhook POST zawiera JSON:
{
"id": "uuid-delivery-id",
"event": "listing.created",
"created_at": "2026-01-01T10:00:00.000Z",
"data": {
// dane ogłoszenia (taki sam format jak GET /listings/:id bez contact)
}
}
Nagłówki żądania webhook
Content-Type: application/json
User-Agent: ZatrudnijMnie-Webhooks/1.0
X-Webhook-Id: uuid-delivery-id
X-Webhook-Event: listing.created
X-Webhook-Timestamp: 1735689600000
X-Webhook-Signature: t=1735689600000,v1=abc123...
Weryfikacja sygnatury
Sygnatura w nagłówku X-Webhook-Signature ma format t={timestamp},v1={hmac}, gdzie:
secretHash = SHA256(secret) // secret dostajesz tylko raz przy tworzeniu webhooka
signedPayload = "{timestamp}.{rawBody}"
hmac = HMAC-SHA256(secretHash, signedPayload)
signature = "t={timestamp},v1={hmac}"
Node.js:
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const timestamp = parseInt(parts.t, 10);
// Sprawdź tolerancję czasową (ochrona przed replay attacks)
if (Math.abs(Date.now() / 1000 - timestamp / 1000) > toleranceSec) {
throw new Error('Webhook timestamp too old');
}
const secretHash = crypto.createHash('sha256').update(secret).digest('hex');
const expected = crypto
.createHmac('sha256', secretHash)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(parts.v1, 'hex'),
Buffer.from(expected, 'hex')
);
}
Python:
import hmac, hashlib, time
def verify_webhook(raw_body: str, signature_header: str, secret: str, tolerance_sec: int = 300) -> bool:
parts = dict(p.split('=', 1) for p in signature_header.split(','))
timestamp = int(parts['t'])
if abs(time.time() - timestamp / 1000) > tolerance_sec:
raise ValueError('Webhook timestamp too old')
secret_hash = hashlib.sha256(secret.encode()).hexdigest()
expected = hmac.new(
secret_hash.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(parts['v1'], expected)
Polityka retry
W przypadku błędu (status != 2xx lub timeout 10s) system ponowi próbę:
| Próba | Opóźnienie |
|---|---|
| 1 (oryginalna) | — |
| 2 | 1 minuta |
| 3 | 5 minut |
| 4 | 15 minut |
Po 4 nieudanych próbach webhook jest oznaczany jako failed.
Max rozmiar payloadu: 64 KB.
Kody błędów
| Kod | HTTP | Opis |
|---|---|---|
API_KEY_MISSING |
401 | Brak klucza API |
API_KEY_INVALID |
401 | Nieprawidłowy lub wygasły klucz |
PERMISSION_DENIED |
403 | Brak wymaganych uprawnień |
RATE_LIMIT_EXCEEDED |
429 | Przekroczono limit requestów |
NOT_FOUND |
404 | Zasób nie znaleziony |
INVALID_ID |
400 | Nieprawidłowy format ID |
VALIDATION_ERROR |
400 | Błąd walidacji danych |
CV_NOT_AVAILABLE |
404 | Ogłoszenie nie zawiera CV |
CV_ERROR |
500 | Błąd generowania linku do CV |
INTERNAL_ERROR |
500 | Wewnętrzny błąd serwera |
Przykłady
cURL
# Lista ogłoszeń zdalnych w IT
curl -X GET "https://zatrudnijmnie.pl/api/v1/listings?work_mode=remote&limit=10" \
-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 = {}) {
const params = new URLSearchParams(filters);
const response = await fetch(`${BASE_URL}/listings?${params}`, {
headers: {
'Authorization': `Bearer ${API_KEY}`
}
});
return response.json();
}
// Użycie
const candidates = await getListings({
work_mode: 'remote',
experience: '5+',
limit: 50
});
Python (requests)
import requests
import os
API_KEY = os.environ['ZATRUDNIJMNIE_API_KEY']
BASE_URL = 'https://zatrudnijmnie.pl/api/v1'
headers = {
'Authorization': f'Bearer {API_KEY}'
}
# Lista ogłoszeń
response = requests.get(
f'{BASE_URL}/listings',
headers=headers,
params={
'work_mode': 'remote',
'experience': '5+',
'limit': 50
}
)
candidates = response.json()
# Szczegóły kandydata
listing_id = candidates['data'][0]['id']
detail = requests.get(
f'{BASE_URL}/listings/{listing_id}',
headers=headers
).json()
print(f"Kontakt: {detail['data']['contact']['email']}")
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
- 🐛 Zgłaszanie błędów: https://github.com/zatrudnijmnie/api-issues
Ostatnia aktualizacja: Kwiecień 2026