v1
REST API · v1

2hr API

A fast, reliable API for publishing and managing job listings. Built for developers who value clarity, speed, and predictable behaviour.

RESTful
Architecture
10/min
Rate limit per token
JSON.
Request & Response
202
Async job creation

Quick Start

Get your first API call running in under 2 minutes.

1
Get your API token

Fill in the registration form. After manual verification (1-2 business days) your Bearer token will be emailed to you. Tokens are account-scoped and never expire unless revoked.

2
Create a company
cURL
curl -X POST https://api.2hr.pl/api/v1/companies \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Example Company Sp. z o.o.","website":"https://example-company.com"}'
3
Post a job linked to that company
cURL
curl -X POST https://api.2hr.pl/api/v1/jobs \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Senior PHP Developer",
    "location": "Warszawa",
    "snippet": "We are looking for...",
    "link": "https://example-company.com/jobs/php",
    "company_uuid": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
    "salary": { "type": "monthly", "min": 12000, "max": 18000, "currency": "PLN" }
  }'
🔐

Get API Token

Tokens are issued after manual verification. Fill in the registration form - we review your details and send the token by email within 1-2 business days.

Registration form: https://api.2hr.pl/register/ -available in Polish and English.

What to fill in

The form asks for basic contact information. Select your account type -the required fields differ:

FieldIndividualCompanyNotes
Name✓ required✓ requiredFull name or company name, max 255 characters.
Email✓ required✓ requiredToken is delivered to this address.
NIP / Tax Number-✓ requiredTax identifier in any country format. Accepted characters: letters, digits, spaces, hyphens, dots and slashes -separators are stripped before storage. 2-30 characters after stripping. Examples: 5260250274 (PL), GB 123 456 789 (UK), B-58378431 (ES), FR 12 345 678 901 (FR), DE123456789 (DE).
Street address-✓ requiredStreet and building / apartment number.
Postal code-✓ required
City-✓ required
Country-✓ required

How the token is delivered

After your account is verified, a 64-character hex token (256-bit entropy) is generated and sent to your email address. Only the SHA-256 hash is stored on the server - the raw token is never saved and cannot be recovered. If you lose it, contact us to issue a new one.

⚠️
Save the token immediately. It appears in the email exactly once. Store it in a password manager or a secure environment variable -never in source code or public repositories.

Using the token

HTTP Header
Authorization: Bearer <your_token>

Re-registration & rate limiting

Submitting the form with an existing email updates your profile data - it does not generate a new token. To get a new token contact us directly. The registration endpoint is rate-limited per IP to prevent abuse.

🌐

Base URL

Base URL
https://api.2hr.pl
📄 Content-Type

All requests and responses use application/json. Always set the Content-Type header on POST and PUT requests.

🔒 HTTPS only

The API is only available over HTTPS. HTTP requests will be refused. All data in transit is encrypted.

🔑

Authentication

Every API request must include a valid Bearer token in the Authorization header.

Bearer Token

Header
Authorization: Bearer <your_token>
⚠️
Keep your token secret. Never expose it in client-side code, public repos, or logs. If compromised, contact us immediately.

Authentication Errors

ScenarioHTTP StatusResponse
No Authorization header401{"error":"Unauthorized","message":"Bearer token required."}
Token not recognised401{"error":"Unauthorized","message":"Invalid token."}

Rate Limiting

Each token is limited to 10 requests per minute using a fixed 60-second window.

📊 Limit

10 requests per token per 60-second window.

🔄 Window

Fixed window -60 seconds. The counter resets at the start of each window.

429 Response

Response · 429 Too Many Requests
{
  "error":        "Too Many Requests",
  "retry_after":  42  // seconds until the window resets
}
⚙️

Endpoints

All endpoints are under /api/v1/ and require a valid Bearer token.

🏢

Companies

Companies represent the hiring organisations behind job listings. Create a company first, then attach it to jobs via company_uuid.

GET /api/v1/companies List all companies for your account Sync · 200

Returns all companies owned by your token's account, ordered by creation date descending.

{
  "data": [
    {
      "uuid":         "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
      "name":        "Example Company Sp. z o.o.",
      "description": "Tech company based in Warsaw.",
      "website":     "https://example-company.com",
      "logo_url":    "https://example-company.com/logo.png",
      "created_at":  "2026-02-28T10:00:00+00:00",
      "updated_at":  "2026-02-28T10:00:00+00:00"
    }
  ],
  "meta": { "total": 1 }
}
curl https://api.2hr.pl/api/v1/companies \
  -H "Authorization: Bearer YOUR_TOKEN"
POST /api/v1/companies Create a new company Sync · 201

Request Body

FieldTypeDescription
name requiredstringCompany name. Max 255 characters.
description optionalstringShort company description.
website optionalstring (URL)Company website URL. Max 500 characters.
logo_url optionalstring (URL)Logo image URL. Max 500 characters.
{
  "name":        "Example Company Sp. z o.o.",
  "description": "Tech company based in Warsaw.",
  "website":     "https://example-company.com",
  "logo_url":    "https://example-company.com/logo.png"
}
{
  "data": {
    "uuid":         "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
    "name":        "Example Company Sp. z o.o.",
    "description": "Tech company based in Warsaw.",
    "website":     "https://example-company.com",
    "logo_url":    "https://example-company.com/logo.png",
    "created_at":  "2026-02-28T10:00:00+00:00",
    "updated_at":  "2026-02-28T10:00:00+00:00"
  }
}
curl -X POST https://api.2hr.pl/api/v1/companies \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Example Company Sp. z o.o.","website":"https://example-company.com"}'
PUT /api/v1/companies/{uuid} Update a company Sync · 200

Updates the company details. All fields are sent -omitted optional fields are set to null.

⚠️
You can only update companies that belong to your account. Attempting to update another account's company returns 404 Not Found.
{
  "name":    "Example Company S.A.",
  "website": "https://example-company.com"
}
{
  "data": {
    "uuid":        "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
    "name":        "Example Company S.A.",
    "description": null,
    "website":     "https://example-company.com",
    "logo_url":    null,
    "created_at":  "2026-02-28T10:00:00+00:00",
    "updated_at":  "2026-02-28T12:00:00+00:00"
  }
}
curl -X PUT https://api.2hr.pl/api/v1/companies/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Example Company S.A.","website":"https://example-company.com"}'
💼

Jobs

GET /api/v1/jobs List all published job listings Sync · 200

Returns all published jobs owned by your account, ordered by updated_at descending. Jobs pending moderation (requires_review) or rejected are not included.

{
  "data": [
    {
      "uuid":    "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "company": {
        "uuid":        "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
        "name":        "Example Company Sp. z o.o.",
        "description": "Tech company based in Warsaw.",
        "website":     "https://example-company.com",
        "logo_url":    "https://example-company.com/logo.png",
        "created_at":  "2026-02-28T10:00:00+00:00",
        "updated_at":  "2026-02-28T10:00:00+00:00"
      },
      "title":  "Senior PHP Developer",
      "location":    "Warszawa",
      "snippet":     "We are looking for...",
      "salary": {
        "type":     "monthly",
        "min":      12000,
        "max":      18000,
        "currency": "PLN"
      },
      "link":        "https://example-company.com/jobs/php",
      "status":      "published",
      "created_at":  "2026-02-28T10:00:00+00:00",
      "updated_at":  "2026-02-28T10:05:00+00:00"
    }
  ],
  "meta": { "total": 1 }
}
curl https://api.2hr.pl/api/v1/jobs \
  -H "Authorization: Bearer YOUR_TOKEN"
POST /api/v1/jobs Create a new job listing Async · 202

Creates a new job listing. The request is accepted immediately and processed asynchronously -the job will be visible once processing completes (typically within seconds).

⚠️
Only verified accounts can post jobs. Unverified accounts receive 403 Forbidden. Contact us to get your account verified.

Request Body

FieldTypeDescription
title requiredstringJob title. Max 255 characters.
location requiredstringJob location (city or remote). Max 255 characters.
snippet requiredstringShort job description / teaser text.
link requiredstring (URL)URL of the recruitment/application form. Max 500 characters.
salary requiredobjectSalary details. See Salary Object.
company_uuid optionalstring (UUID v4)UUID of the company to associate with this job.
source optionalstringTraffic source label (e.g. "linkedin").
type optionalstringEmployment type (e.g. "full-time", "contract").
{
  "title":      "Senior PHP Developer",
  "location":   "Warszawa",
  "snippet":    "We are looking for an experienced PHP developer...",
  "link":       "https://example-company.com/jobs/php",
  "company_uuid": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
  "salary": {
    "type":     "monthly",
    "min":      12000,
    "max":      18000,
    "currency": "PLN"
  },
  "type":       "full-time"
}
{
  "uuid":   "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "status": "pending"  // becomes "published" once processed
}
{
  "error": "Field \"salary.min\" cannot be greater than \"salary.max\"."
}
curl -X POST https://api.2hr.pl/api/v1/jobs \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Senior PHP Developer",
    "location": "Warszawa",
    "snippet": "We are looking for...",
    "link": "https://example-company.com/jobs/php",
    "company_uuid": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
    "salary": {"type":"monthly","min":12000,"max":18000,"currency":"PLN"}
  }'
PUT /api/v1/jobs/{uuid} Update a job listing Sync · 200

Updates the content of an existing job. Accepts the same fields as POST. Only jobs owned by your account can be updated.

⚠️
Jobs with status ended cannot be updated and will return 409 Conflict.
{
  "title":    "Lead PHP Developer",
  "location": "Warszawa / Remote",
  "snippet":  "Updated description...",
  "link":     "https://example-company.com/jobs/php",
  "salary": {
    "type":     "monthly",
    "min":      15000,
    "max":      22000,
    "currency": "PLN"
  }
}
{
  "data": {
    "uuid":   "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "title":  "Lead PHP Developer",
    "salary": {
      "type":     "monthly",
      "min":      15000,
      "max":      22000,
      "currency": "PLN"
    },
    "status": "published"
  }
}
curl -X PUT https://api.2hr.pl/api/v1/jobs/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Lead PHP Developer","location":"Warszawa","snippet":"...","link":"https://example-company.com/jobs/php","salary":{"type":"monthly","min":15000,"max":22000,"currency":"PLN"}}'
PUT /api/v1/jobs/{uuid}/status Change a job's status Sync · 200

Transitions a job to a new status. Only valid transitions are allowed -see Job Status.

Request Body

FieldTypeDescription
status requiredstring (enum)Target status. One of: published, ended.
{ "status": "ended" }
{
  "data": {
    "uuid":      "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "status":     "ended",
    "updated_at": "2026-02-28T12:00:00+00:00"
  }
}
curl -X PUT https://api.2hr.pl/api/v1/jobs/f47ac10b-58cc-4372-a567-0e02b2c3d479/status \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"ended"}'
🏢

Company Object

FieldTypeDescription
uuidstring (UUID v4)Public unique identifier. Use this value in PUT requests.
namestringCompany name. Max 255 characters.
descriptionstring | nullOptional short description.
websitestring | nullCompany website URL.
logo_urlstring | nullLogo image URL.
created_atstring (ISO 8601)Creation timestamp.
updated_atstring (ISO 8601)Last update timestamp.
💼

Job Object

FieldTypeDescription
uuidstring (UUID v4)Public unique identifier. Use this value in PUT requests.
companyobject | nullEmbedded company data (see Company Object), or null if no company is linked.
titlestringJob title.
locationstringJob location.
snippetstringShort job description.
salaryobjectSalary details. See Salary Object.
linkstringURL of the recruitment/application form.
sourcestring | nullTraffic source label.
typestring | nullEmployment type.
statusstring (enum)Current status: pending, published, ended, failed, requires_review, rejected.
created_atstring (ISO 8601)Creation timestamp.
updated_atstring (ISO 8601)Last update timestamp.
💰

Salary Object

The salary field is required on all job create and update requests. It is an object with the following fields:

FieldTypeDescription
typestring (enum)Pay period. One of: monthly, hourly.
minintegerMinimum salary. Must be ≥ 0 and ≤ max.
maxintegerMaximum salary. Must be ≥ min.
currencystringISO 4217 currency code, 3 uppercase letters (e.g. PLN, EUR, USD).

Examples

Monthly
{
  "type":     "monthly",
  "min":      12000,
  "max":      18000,
  "currency": "PLN"
}
Hourly
{
  "type":     "hourly",
  "min":      100,
  "max":      160,
  "currency": "PLN"
}
⚠️
Validation rules: min cannot be greater than max. Currency must be exactly 3 uppercase letters (ISO 4217). Lowercase or mixed-case (e.g. pln) will be rejected with 422.
🔄

Job Status & Lifecycle

Jobs follow a strict state machine. Invalid transitions are rejected with HTTP 422.

pending
──▶
publishedProcessing succeeded
pending
──▶
failedProcessing error (automatic)
requires_review
──▶
publishedApproved by moderator
requires_review
──▶
rejectedRejected by moderator
published
──▶
endedManually closed via API
failed
──▶
Terminal state. No further transitions.
ended
──▶
Terminal state. No further transitions.
rejected
──▶
Terminal state. No further transitions.

Error Codes

All errors return a consistent JSON body. The error field is always present.

Error shape
{
  "error":   "Descriptive error",  // always present
  "message": "More detail"          // auth errors
}
HTTP StatusMeaningWhen it happens
200 OKSuccessGET and synchronous PUT requests completed successfully.
201 CreatedCreatedCompany created successfully.
202 AcceptedAcceptedJob creation accepted for async processing.
400 Bad RequestMalformed requestRequest body is not valid JSON or missing Content-Type header.
401 UnauthorizedAuthentication failedMissing, malformed, or invalid Bearer token.
403 ForbiddenAccess deniedResource exists but belongs to a different account.
404 Not FoundResource not foundJob or company does not exist.
422 UnprocessableValidation failedMissing field, invalid URL, invalid salary (min > max, bad currency), illegal status transition.
429 Too Many RequestsRate limit exceededMore than 10 requests in the current 60-second window.
500 Internal ErrorServer errorUnexpected server-side failure. Retry after a short delay.

Async Processing

Job creation (POST /api/v1/jobs) uses asynchronous processing. Your request returns immediately with HTTP 202 while the job is processed in the background.

Your App
POST /api/v1/jobs
HTTP 202
API
saves + enqueues
message queue
Queue
async transport
worker picks up
Worker
publishes job
✅ Immediately

You get the job UUID back with status pending. Poll GET /api/v1/jobs to check when it becomes published.

⏱ Typically

Within seconds. Status transitions from pendingpublished automatically once processing completes.

📐

OpenAPI Specification

The full API is described in an OpenAPI 3.1 spec compatible with Postman, Insomnia, and Swagger Editor.

📥 Postman

Open Postman → ImportLink → paste the URL below.

URL
https://api.2hr.pl/docs/openapi.yaml
🌙 Insomnia

Open Insomnia → ImportFrom URL → paste the URL below.

URL
https://api.2hr.pl/docs/openapi.yaml

Swagger Editor

Swagger Editor cannot load the spec via URL due to browser CORS restrictions. Download the file first and paste the contents into the editor.

1
Open the spec in your browser
URL
https://api.2hr.pl/docs/openapi.yaml
2
Select all and copy

Press Ctrl+A then Ctrl+C.

3
Paste into Swagger Editor

Go to editor.swagger.io and paste (Ctrl+V) into the left panel.

The spec file is always in sync with this documentation. If you notice a discrepancy, let us know.