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

  1. Wprowadzenie
  2. Autoryzacja
  3. Rate Limiting
  4. Format odpowiedzi
  5. Endpoints
  6. Kody błędów
  7. Przykłady

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

  1. Zaloguj się do konta zespołu
  2. Przejdź do Ustawienia → Dostęp / API
  3. Kliknij "Utwórz nowy klucz"
  4. 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-Origin jest 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 cvUrl w odpowiedzi GET /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łoszenie
  • listing.updated — aktualizacja ogłoszenia
  • listing.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.ping z 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


Ostatnia aktualizacja: Kwiecień 2026