A sample client for fetching public job offers from the SOLID.Jobs portal.
The API is designed for external integrators, job aggregators, and anyone who wants to build custom interfaces or IT market analyses.
- No classic authorization — The API does not require API Keys or OAuth tokens. Access is controlled via a mandatory
campaignparameter used for traffic tracking (see below). - Advanced filtering — Search by cities, technologies, experience levels, and salary ranges.
- Pagination and sorting — Full control over the data you retrieve.
- Standardization — Clear and predictable data format (JSON).
GET https://solid.jobs/public-api/offers/{division}?campaign=my-awesome-aggregatorAvailable divisions: IT, Engineering, Marketing, Sales, HR, Logistics, Finances, Other.
The API is versioned. The current version is 1.0. You can specify the version in the request header:
X-Api-Version: 1.0Omitting the header means the latest available version will be used.
Every request must include the campaign query parameter. It is used solely for traffic analytics — it is not an authorization token.
- Value: Your unique identifier (e.g. company name, bot name, integration id).
- Format: Lowercase letters, digits, and hyphens only. Maximum 64 characters.
- Example:
?campaign=my-awesome-aggregator
The API enforces rate limiting. Exceeding the limit returns a 429 Too Many Requests status.
| Limit | Value |
|---|---|
| Requests per minute (per IP) | 300 (fixed window) |
| Queue limit | 10 |
We recommend handling the 429 status with a retry mechanism (e.g. exponential backoff).
For a detailed list of allowed values for each parameter, see DICTIONARIES.md.
| Parameter | Type | Description | Example |
|---|---|---|---|
pageIndex |
int | Page index (starting from 0, default 0). | 0 |
pageSize |
int | Page size (default 30, max 500). | 50 |
sortActive |
string | Sort field (validFrom, validTo, title, company, salaryFrom, salaryTo, experienceLevel). |
validFrom |
sortDirection |
string | Sort direction (asc or desc). |
desc |
search.cities |
string | Comma-separated cities. | Poznań,Warszawa |
search.categories |
string[] | Main categories (e.g. Developer, Tester). |
Developer |
search.subCategories |
string[] | Subcategories (e.g. DotNet, Java). |
DotNet,Java |
search.experiences |
string[] | Experience level. | Regular,Senior |
search.searchTerm |
string[] | Full-text search phrases. | Angular |
search.minimumSalary |
int | Minimum salary — lower bound of salary range ≥ value. | 20000 |
A successful 200 OK response returns a JSON object with the following structure:
{
"pageIndex": 0,
"pageSize": 30,
"totalCount": 142,
"totalPages": 5,
"jobs": [
{
"jobOfferKey": "abc-123-def",
"title": "Senior .NET Developer",
"division": "IT",
"category": "Developer",
"subCategory": "DotNet",
"company": "Acme Corp",
"companyLogoUrl": "https://solid.jobs/images/company/acme.png",
"salary": {
"from": 18000,
"to": 25000,
"currency": "PLN",
"period": "Month",
"employmentType": "B2B"
},
"secondarySalary": {
"from": 15000,
"to": 20000,
"currency": "PLN",
"period": "Month",
"employmentType": "UoP"
},
"contractTime": "full_time",
"locations": ["Warszawa", "Kraków"],
"benefits": ["Private healthcare", "Sport card"],
"isRemote": true,
"isHybrid": false,
"url": "https://solid.jobs/offer/abc-123-def",
"experienceLevel": "Senior",
"skills": [
{ "name": ".NET", "level": "Advanced" },
{ "name": "Azure", "level": "NiceToHave" }
],
"languages": [
{ "name": "English", "level": "Advanced" }
],
"description": "We are looking for a Senior .NET Developer...",
"validFrom": "2026-05-01T00:00:00+00:00",
"validTo": "2026-06-01T00:00:00+00:00",
"updatedAt": "2026-05-15T12:30:00+00:00"
}
]
}| Field | Description |
|---|---|
pageIndex |
Current page index (starting from 0). |
pageSize |
Number of offers per page. |
totalCount |
Total number of offers matching the query. |
totalPages |
Total number of pages. |
jobs |
Array of job offer objects. |
| Field | Description |
|---|---|
jobOfferKey |
Unique offer identifier. |
title |
Job title. |
division |
Division (e.g. IT, Engineering). |
category |
Main category (e.g. Developer). |
subCategory |
Subcategory (e.g. DotNet). |
company |
Company name. |
companyLogoUrl |
Company logo URL (nullable). |
salary |
Primary salary object. |
secondarySalary |
Secondary salary object, e.g. different contract type (nullable). |
contractTime |
Contract time (e.g. full_time, part_time). |
locations |
Array of city names. |
benefits |
Array of benefit descriptions. |
isRemote |
Whether the position is fully remote. |
isHybrid |
Whether the position is hybrid. |
url |
Direct link to the offer on SOLID.Jobs. |
experienceLevel |
Required experience level. |
skills |
Array of required skills (name + level). |
languages |
Array of required languages (name + level). |
description |
Job offer description. |
validFrom |
Offer publication date. |
validTo |
Offer expiration date. |
updatedAt |
Last update timestamp (nullable). |
| Field | Description |
|---|---|
from |
Lower bound of salary range (nullable). |
to |
Upper bound of salary range (nullable). |
currency |
Currency code (e.g. PLN, EUR, USD). |
period |
Payment period (e.g. Month, Hour). |
employmentType |
Employment type (e.g. B2B, UoP). |
400 Bad Request — invalid or missing campaign:
Make sure that campaign parameter exists and contains only lowercase letters, numbers and dashes (max 64 chars long).
400 Bad Request — invalid division:
Division not allowed: 'InvalidValue'. Avialable values are: IT, Engineering, Marketing, Sales, HR, Logistics, Finances, Other.
429 Too Many Requests — rate limit exceeded. Retry after a short delay.
Aggregated labour-market statistics for a single scope — a division, a main category, a specialization (subcategory), a subcategory group, or a city. Like the offers endpoint it needs no authorization (only the campaign parameter) and returns a flat, stable JSON contract. Responses are cacheable for up to 1 hour.
GET https://solid.jobs/public-api/market-statistics/{scopeKind}/{scopeKey}?campaign=my-awesome-aggregatorThe same X-Api-Version: 1.0 header, the campaign rules, and the rate limits (300 req/min per IP, queue 10) described above apply here too.
| Parameter | Type | Description |
|---|---|---|
scopeKind |
string | Kind of scope (case-insensitive). One of division, mainCategory, subcategory, subcategoryGroup, city. |
scopeKey |
string | Concrete value within the kind — see the mapping below. |
scopeKey allowed values (an unknown kind or key returns 404):
scopeKind |
scopeKey value |
Example | Allowed values |
|---|---|---|---|
division |
Division name | IT |
DICTIONARIES §2 |
mainCategory |
Main category name | Developer |
DICTIONARIES §3 (categories) |
subcategory |
Subcategory name | React |
DICTIONARIES §3 (subcategories) |
subcategoryGroup |
Subcategory group | Frontend |
Frontend, Mobile (DICTIONARIES §9) |
city |
City slug (lowercased) | warszawa |
Any city served by the portal |
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign |
string | yes | Traffic identifier — lowercase letters, digits and hyphens, max 64 chars. |
fields |
string | no | Comma-separated subset of sections to return (case-insensitive): demand, salary, experience, topLocations, topSkills. When omitted, all sections available for the scope are returned. |
topLocations is not available for the city scope (the scope is already a single city). Requesting it explicitly for a city returns 400; omitting fields simply skips it. All other sections are available for every scope kind.
A successful 200 OK response for subcategory/React (all sections):
{
"scopeKind": "subcategory",
"scopeKey": "React",
"generatedAt": "2026-07-08T09:15:00.1234567+00:00",
"includedSections": ["demand", "salary", "experience", "topLocations", "topSkills"],
"demand": {
"activeOffers": 312,
"distinctEmployers": 148,
"remoteOffers": 121,
"remotePercentage": 39,
"offerTrend": [
{ "period": "2025-Q2", "offerCount": 268 },
{ "period": "2025-Q3", "offerCount": 274 },
{ "period": "2025-Q4", "offerCount": 289 },
{ "period": "2026-Q1", "offerCount": 312 }
]
},
"salary": {
"currency": "PLN",
"overall": { "min": 8000, "p25": 14000, "median": 18000, "p75": 23000, "max": 38000 },
"b2b": { "median": 20000, "average": 20450, "offerCount": 176 },
"permanent": { "median": 15000, "average": 15200, "offerCount": 92 }
},
"experience": [
{ "label": "Senior", "offerCount": 168, "percentage": 54 },
{ "label": "Regular", "offerCount": 108, "percentage": 35 },
{ "label": "Junior", "offerCount": 36, "percentage": 11 }
],
"topLocations": [
{ "label": "Warszawa", "offerCount": 98, "percentage": 31 },
{ "label": "Kraków", "offerCount": 54, "percentage": 17 },
{ "label": "Wrocław", "offerCount": 41, "percentage": 13 }
],
"topSkills": [
{ "label": "React", "offerCount": 312, "percentage": 100 },
{ "label": "TypeScript", "offerCount": 254, "percentage": 81 },
{ "label": "Redux", "offerCount": 120, "percentage": 38 }
]
}| Field | Type | Description |
|---|---|---|
scopeKind |
string | Scope kind the statistics describe (echoes the request, normalized). |
scopeKey |
string | Scope key within the kind. Enum-based kinds keep canonical casing; city is a lowercased slug. |
generatedAt |
string | Generation timestamp (UTC, ISO-8601 with a +00:00 offset). |
includedSections |
string[] | Section names actually present in this response — lets you confirm what came back when some were skipped. |
demand |
object | Demand & hiring metrics. Omitted when the section was not requested. |
salary |
object | Salary metrics. Omitted when the section was not requested. |
experience |
object[] | Experience-level distribution. Omitted when not requested. |
topLocations |
object[] | Top cities distribution. Omitted when not requested or unavailable (city scope). |
topSkills |
object[] | Top skills distribution. Omitted when not requested. |
| Field | Type | Description |
|---|---|---|
activeOffers |
int | Number of active offers in the scope. |
distinctEmployers |
int | Number of unique employers publishing in the scope. |
remoteOffers |
int | Number of fully remote offers. |
remotePercentage |
int | Share of fully remote offers (0–100). |
offerTrend |
object[] | Quarterly offer-count trend (precomputed), up to the 8 most recent quarters, oldest first. |
| Field | Type | Description |
|---|---|---|
period |
string | Quarter label in YYYY-Qn format (e.g. 2026-Q1). |
offerCount |
int | Number of offers in that quarter. |
| Field | Type | Description |
|---|---|---|
currency |
string | Currency of every amount below. Always PLN. |
overall |
object | null | Salary band computed live from active offers (percentiles). null when no offer in the scope declares a salary. |
b2b |
object | null | Precomputed B2B salary stat. null when there is no precomputed data. |
permanent |
object | null | Precomputed permanent-contract (UoP) salary stat. null when there is no precomputed data. |
| Field | Type | Description |
|---|---|---|
min |
number | Minimum — lower edge of the range. |
p25 |
number | 25th percentile. |
median |
number | Median (50th percentile). |
p75 |
number | 75th percentile. |
max |
number | Maximum — upper edge of the range. |
| Field | Type | Description |
|---|---|---|
median |
number | Median salary for the contract type. |
average |
number | Average salary for the contract type. |
offerCount |
int | Number of offers behind the stat. |
All three sections share the same bucket shape:
| Field | Type | Description |
|---|---|---|
label |
string | Entry label — experience level (experience), city name (topLocations) or skill name (topSkills). |
offerCount |
int | Number of active offers in this entry. |
percentage |
int | Share against all active offers of the scope (0–100). |
400 Bad Request — invalid or missing campaign:
Make sure that campaign parameter exists and contains only lowercase letters, numbers and dashes (max 64 chars long).
400 Bad Request — unknown section in fields:
Unknown section 'foo'. Available sections: Demand, Salary, Experience, TopLocations, TopSkills.
400 Bad Request — a requested section is not available for the scope (e.g. topLocations for a city):
Section(s) not available for scope kind 'City': TopLocations. Available for this scope: Demand, Salary, Experience, TopSkills.
404 Not Found — unknown scopeKind or scopeKey (empty body).
429 Too Many Requests — rate limit exceeded. Retry after a short delay.
A year-by-year market report for a single role — offer volume, contract-type split, seniority split and salary levels, one entry per calendar year. Unlike the statistics endpoint above there is no scopeKind (the path segment is always raport) and no fields parameter — the report is always returned whole. Responses are cacheable for up to 1 hour.
GET https://solid.jobs/public-api/market-statistics/raport/{scopeKey}?campaign=my-awesome-aggregatorThe same X-Api-Version: 1.0 header, the campaign rules, and the rate limits (300 req/min per IP, queue 10) described above apply here too.
| Parameter | Type | Description |
|---|---|---|
scopeKey |
string | Role (specialization) the report describes, e.g. ManualTester. Case-insensitive; an unknown value returns 404. Allowed values: DICTIONARIES §3 (subcategories). |
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign |
string | yes | Traffic identifier — lowercase letters, digits and hyphens, max 64 chars. |
The report spans up to 3 calendar years, oldest first: the current year plus the two before it. The current year is year-to-date, so its counts are naturally lower than a completed year's.
A successful 200 OK response for Golang (trimmed here to two years, topSkills trimmed to a handful of entries):
{
"scopeKey": "Golang",
"generatedAt": "2026-08-07T07:14:51.6665451+00:00",
"years": [
{
"year": 2024,
"topSkills": [
{ "name": "Golang", "count": 43 },
{ "name": "Kubernetes", "count": 19 },
{ "name": "Docker", "count": 16 },
{ "name": "AWS", "count": 12 },
{ "name": "MongoDB", "count": 10 },
{ "name": "React", "count": 6 }
],
"quarters": [
{
"quarter": 1,
"offerCount": 9,
"contractType": {
"b2bOnly": { "count": 8, "percentage": 89 },
"permanentOnly": { "count": 1, "percentage": 11 },
"both": { "count": 0, "percentage": 0 },
"total": 9
},
"seniority": {
"junior": {
"count": 0,
"percentage": 0,
"contractType": {
"b2bOnly": { "count": 0, "percentage": 0 },
"permanentOnly": { "count": 0, "percentage": 0 },
"both": { "count": 0, "percentage": 0 },
"total": 0
}
},
"regular": {
"count": 5,
"percentage": 56,
"contractType": {
"b2bOnly": { "count": 4, "percentage": 80 },
"permanentOnly": { "count": 1, "percentage": 20 },
"both": { "count": 0, "percentage": 0 },
"total": 5
}
},
"senior": {
"count": 4,
"percentage": 44,
"contractType": {
"b2bOnly": { "count": 4, "percentage": 100 },
"permanentOnly": { "count": 0, "percentage": 0 },
"both": { "count": 0, "percentage": 0 },
"total": 4
}
},
"total": 9
},
"salaryB2B": {
"junior": { "medianLower": 0, "medianUpper": 0, "averageLower": 0, "averageUpper": 0, "salaryRangeCount": 0 },
"regular": { "medianLower": 20500, "medianUpper": 25000, "averageLower": 20100, "averageUpper": 25400, "salaryRangeCount": 4 },
"senior": { "medianLower": 22800, "medianUpper": 26500, "averageLower": 22600, "averageUpper": 27100, "salaryRangeCount": 4 }
}
},
{
"quarter": 2,
"offerCount": 11,
"contractType": {
"b2bOnly": { "count": 10, "percentage": 91 },
"permanentOnly": { "count": 1, "percentage": 9 },
"both": { "count": 0, "percentage": 0 },
"total": 11
},
"seniority": {
"junior": {
"count": 0,
"percentage": 0,
"contractType": {
"b2bOnly": { "count": 0, "percentage": 0 },
"permanentOnly": { "count": 0, "percentage": 0 },
"both": { "count": 0, "percentage": 0 },
"total": 0
}
},
"regular": {
"count": 6,
"percentage": 55,
"contractType": {
"b2bOnly": { "count": 5, "percentage": 83 },
"permanentOnly": { "count": 1, "percentage": 17 },
"both": { "count": 0, "percentage": 0 },
"total": 6
}
},
"senior": {
"count": 5,
"percentage": 45,
"contractType": {
"b2bOnly": { "count": 5, "percentage": 100 },
"permanentOnly": { "count": 0, "percentage": 0 },
"both": { "count": 0, "percentage": 0 },
"total": 5
}
},
"total": 11
},
"salaryB2B": {
"junior": { "medianLower": 0, "medianUpper": 0, "averageLower": 0, "averageUpper": 0, "salaryRangeCount": 0 },
"regular": { "medianLower": 21200, "medianUpper": 25800, "averageLower": 20800, "averageUpper": 26100, "salaryRangeCount": 5 },
"senior": { "medianLower": 23000, "medianUpper": 27200, "averageLower": 23200, "averageUpper": 27800, "salaryRangeCount": 5 }
}
}
],
"offerCount": 43,
"contractType": {
"b2bOnly": { "count": 38, "percentage": 88 },
"permanentOnly": { "count": 4, "percentage": 9 },
"both": { "count": 1, "percentage": 2 },
"total": 43
},
"seniority": {
"junior": {
"count": 0,
"percentage": 0,
"contractType": {
"b2bOnly": { "count": 0, "percentage": 0 },
"permanentOnly": { "count": 0, "percentage": 0 },
"both": { "count": 0, "percentage": 0 },
"total": 0
}
},
"regular": {
"count": 22,
"percentage": 51,
"contractType": {
"b2bOnly": { "count": 20, "percentage": 91 },
"permanentOnly": { "count": 2, "percentage": 9 },
"both": { "count": 0, "percentage": 0 },
"total": 22
}
},
"senior": {
"count": 21,
"percentage": 49,
"contractType": {
"b2bOnly": { "count": 18, "percentage": 86 },
"permanentOnly": { "count": 2, "percentage": 10 },
"both": { "count": 1, "percentage": 5 },
"total": 21
}
},
"total": 43
},
"salaryB2B": {
"junior": { "medianLower": 0, "medianUpper": 0, "averageLower": 0, "averageUpper": 0, "salaryRangeCount": 0 },
"regular": { "medianLower": 21800, "medianUpper": 26000, "averageLower": 20930, "averageUpper": 26325, "salaryRangeCount": 20 },
"senior": { "medianLower": 23500, "medianUpper": 26900, "averageLower": 23068, "averageUpper": 27847, "salaryRangeCount": 19 }
},
"salaryUoP": {
"junior": { "medianLower": 0, "medianUpper": 0, "averageLower": 0, "averageUpper": 0, "salaryRangeCount": 0 },
"regular": { "medianLower": 14500, "medianUpper": 18500, "averageLower": 14500, "averageUpper": 18500, "salaryRangeCount": 2 },
"senior": { "medianLower": 18000, "medianUpper": 22000, "averageLower": 19000, "averageUpper": 23333, "salaryRangeCount": 3 }
}
},
{
"year": 2025,
"topSkills": [
{ "name": "Golang", "count": 42 },
{ "name": "Kubernetes", "count": 20 },
{ "name": "Docker", "count": 13 },
{ "name": "SQL", "count": 10 },
{ "name": "REST", "count": 10 },
{ "name": "CI/CD", "count": 9 }
],
"quarters": [
{
"quarter": 1,
"offerCount": 12,
"contractType": {
"b2bOnly": { "count": 11, "percentage": 92 },
"permanentOnly": { "count": 1, "percentage": 8 },
"both": { "count": 0, "percentage": 0 },
"total": 12
},
"seniority": {
"junior": {
"count": 0,
"percentage": 0,
"contractType": {
"b2bOnly": { "count": 0, "percentage": 0 },
"permanentOnly": { "count": 0, "percentage": 0 },
"both": { "count": 0, "percentage": 0 },
"total": 0
}
},
"regular": {
"count": 7,
"percentage": 58,
"contractType": {
"b2bOnly": { "count": 6, "percentage": 86 },
"permanentOnly": { "count": 1, "percentage": 14 },
"both": { "count": 0, "percentage": 0 },
"total": 7
}
},
"senior": {
"count": 5,
"percentage": 42,
"contractType": {
"b2bOnly": { "count": 5, "percentage": 100 },
"permanentOnly": { "count": 0, "percentage": 0 },
"both": { "count": 0, "percentage": 0 },
"total": 5
}
},
"total": 12
},
"salaryB2B": {
"junior": { "medianLower": 0, "medianUpper": 0, "averageLower": 0, "averageUpper": 0, "salaryRangeCount": 0 },
"regular": { "medianLower": 20800, "medianUpper": 24900, "averageLower": 20950, "averageUpper": 24600, "salaryRangeCount": 6 },
"senior": { "medianLower": 23200, "medianUpper": 26700, "averageLower": 25100, "averageUpper": 29700, "salaryRangeCount": 4 }
},
"salaryUoP": {
"junior": { "medianLower": 0, "medianUpper": 0, "averageLower": 0, "averageUpper": 0, "salaryRangeCount": 0 },
"regular": { "medianLower": 0, "medianUpper": 0, "averageLower": 0, "averageUpper": 0, "salaryRangeCount": 0 },
"senior": { "medianLower": 13650, "medianUpper": 22150, "averageLower": 13650, "averageUpper": 22150, "salaryRangeCount": 2 }
}
},
{
"quarter": 2,
"offerCount": 10,
"contractType": {
"b2bOnly": { "count": 9, "percentage": 90 },
"permanentOnly": { "count": 1, "percentage": 10 },
"both": { "count": 0, "percentage": 0 },
"total": 10
},
"seniority": {
"junior": {
"count": 0,
"percentage": 0,
"contractType": {
"b2bOnly": { "count": 0, "percentage": 0 },
"permanentOnly": { "count": 0, "percentage": 0 },
"both": { "count": 0, "percentage": 0 },
"total": 0
}
},
"regular": {
"count": 5,
"percentage": 50,
"contractType": {
"b2bOnly": { "count": 5, "percentage": 100 },
"permanentOnly": { "count": 0, "percentage": 0 },
"both": { "count": 0, "percentage": 0 },
"total": 5
}
},
"senior": {
"count": 5,
"percentage": 50,
"contractType": {
"b2bOnly": { "count": 4, "percentage": 80 },
"permanentOnly": { "count": 1, "percentage": 20 },
"both": { "count": 0, "percentage": 0 },
"total": 5
}
},
"total": 10
},
"salaryB2B": {
"junior": { "medianLower": 0, "medianUpper": 0, "averageLower": 0, "averageUpper": 0, "salaryRangeCount": 0 },
"regular": { "medianLower": 21400, "medianUpper": 25500, "averageLower": 21300, "averageUpper": 25100, "salaryRangeCount": 5 },
"senior": { "medianLower": 23800, "medianUpper": 27500, "averageLower": 25800, "averageUpper": 30300, "salaryRangeCount": 4 }
}
}
],
"offerCount": 43,
"contractType": {
"b2bOnly": { "count": 40, "percentage": 93 },
"permanentOnly": { "count": 3, "percentage": 7 },
"both": { "count": 0, "percentage": 0 },
"total": 43
},
"seniority": {
"junior": {
"count": 0,
"percentage": 0,
"contractType": {
"b2bOnly": { "count": 0, "percentage": 0 },
"permanentOnly": { "count": 0, "percentage": 0 },
"both": { "count": 0, "percentage": 0 },
"total": 0
}
},
"regular": {
"count": 23,
"percentage": 53,
"contractType": {
"b2bOnly": { "count": 22, "percentage": 96 },
"permanentOnly": { "count": 1, "percentage": 4 },
"both": { "count": 0, "percentage": 0 },
"total": 23
}
},
"senior": {
"count": 20,
"percentage": 47,
"contractType": {
"b2bOnly": { "count": 18, "percentage": 90 },
"permanentOnly": { "count": 2, "percentage": 10 },
"both": { "count": 0, "percentage": 0 },
"total": 20
}
},
"total": 43
},
"salaryB2B": {
"junior": { "medianLower": 0, "medianUpper": 0, "averageLower": 0, "averageUpper": 0, "salaryRangeCount": 0 },
"regular": { "medianLower": 21000, "medianUpper": 25200, "averageLower": 21118, "averageUpper": 24877, "salaryRangeCount": 22 },
"senior": { "medianLower": 23500, "medianUpper": 27100, "averageLower": 25428, "averageUpper": 30028, "salaryRangeCount": 18 }
},
"salaryUoP": {
"junior": { "medianLower": 0, "medianUpper": 0, "averageLower": 0, "averageUpper": 0, "salaryRangeCount": 0 },
"regular": { "medianLower": 19800, "medianUpper": 33400, "averageLower": 19800, "averageUpper": 33400, "salaryRangeCount": 1 },
"senior": { "medianLower": 13650, "medianUpper": 22150, "averageLower": 13650, "averageUpper": 22150, "salaryRangeCount": 2 }
}
}
]
}| Field | Type | Description |
|---|---|---|
scopeKey |
string | Role the report describes (echoes the request, canonical casing). |
generatedAt |
string | Generation timestamp (UTC, ISO-8601 with a +00:00 offset). |
years |
object[] | One entry per calendar year, oldest first. |
| Field | Type | Description |
|---|---|---|
year |
int | Calendar year the entry describes. |
topSkills |
object[] | Most required skills that year, descending by count, up to 100 entries. Empty array (never omitted) when there's no data. |
quarters |
object[] | Per-quarter breakdown of the same year, oldest first. See quarters[] fields below — each entry has the same contractType/seniority/salaryB2B/salaryUoP shape as the year itself (no topSkills). |
offerCount |
int | All offers published in the role that year. |
contractType |
object | Split by the contract types an offer proposes. |
seniority |
object | Split by required experience level, with a contract-type split nested inside each level. |
salaryB2B |
object | B2B salary levels for the year, by seniority. Omitted when the year has no B2B salary data at all. |
salaryUoP |
object | Permanent-contract (UoP) salary levels, by seniority. Omitted when the year has no UoP salary data at all. |
| Field | Type | Description |
|---|---|---|
b2bOnly |
object | Offers proposing only B2B. |
permanentOnly |
object | Offers proposing only a permanent contract (UoP). |
both |
object | Offers proposing both B2B and UoP. |
total |
int | Sum of the three buckets — the denominator of their percentage. Read the note below: this is not offerCount. |
Every bucket in a contractType breakdown — whether at year level or nested inside a seniority entry — shares the same shape:
| Field | Type | Description |
|---|---|---|
count |
int | Number of offers in this bucket. |
percentage |
int | Share against the breakdown's own total (0–100). |
| Field | Type | Description |
|---|---|---|
junior |
object | Offers requiring a junior level — count, percentage, and their own contract-type split. |
regular |
object | Offers requiring a regular level — count, percentage, and their own contract-type split. |
senior |
object | Offers requiring a senior level — count, percentage, and their own contract-type split. |
total |
int | Sum of the three levels' count — the denominator of their percentage. Read the note below: this is not offerCount. |
| Field | Type | Description |
|---|---|---|
count |
int | Number of offers at this seniority level. |
percentage |
int | Share against seniority.total (0–100). |
contractType |
object | Contract-type split within this seniority level only — same shape as the year-level contractType, with its own independent total. |
When present, both objects are keyed by seniority (junior, regular, senior) rather than being a single flat stat — every present object carries all three keys, even for a seniority with no matching offers that year (see the note below).
| Field | Type | Description |
|---|---|---|
junior |
object | Salary band for junior offers that year. |
regular |
object | Salary band for regular offers that year. |
senior |
object | Salary band for senior offers that year. |
| Field | Type | Description |
|---|---|---|
medianLower |
number | Lower edge of the median salary range in PLN. |
medianUpper |
number | Upper edge of the median salary range in PLN. |
averageLower |
number | Lower edge of the average salary range in PLN. |
averageUpper |
number | Upper edge of the average salary range in PLN. |
salaryRangeCount |
int | Number of salary ranges behind the figures for this seniority — not a distinct offer count, see the note below. All fields are 0 when this seniority has no salary data that year. |
| Field | Type | Description |
|---|---|---|
name |
string | Skill name. |
count |
int | Number of offers requiring this skill that year — not a count of occurrences. |
One entry per calendar quarter within the year, oldest first. Every field below has the exact same meaning, shape, and omission rules as its year-level counterpart (see contractType, seniority, salaryB2B / salaryUoP above) — just scoped to that quarter instead of the whole year. There is no topSkills at this level.
| Field | Type | Description |
|---|---|---|
quarter |
int | Quarter number, 1–4. |
offerCount |
int | All offers published in the role that quarter. |
contractType |
object | Same shape as the year-level contractType, with its own independent total. |
seniority |
object | Same shape as the year-level seniority, with its own independent total. |
salaryB2B |
object | Same shape as the year-level salaryB2B. Omitted when the quarter has no B2B salary data at all. |
salaryUoP |
object | Same shape as the year-level salaryUoP. Omitted when the quarter has no UoP salary data at all. |
Several things will trip up a naive integration:
totalis notofferCount, at every level. An offer proposing neither B2B nor a permanent contract (a mandate contract, for instance) lands in nocontractTypebucket, and an offer with no declared experience level lands in nosenioritybucket. The year'scontractType.total,seniority.total, and thecontractType.totalnested inside each seniority level are all independent denominators — none of them equalofferCountor each other. Always divide apercentageby thetotalof the object it lives in.- Percentages are rounded independently, so the three values of a breakdown can add up to 99 or 101 rather than exactly 100.
- Salary figures are a band, not a point estimate.
medianLower/medianUpperandaverageLower/averageUppercome from pooling both ends of every matching salary range, not a single number — readsalaryB2B.regular.medianLower–medianUpperas the range the median falls in for regular B2B offers that year. - A present
salaryB2B/salaryUoPalways has all three seniority keys. A seniority with zero matching offers that year still appears with every field at0— only the wholesalaryB2B/salaryUoPobject is omitted (when there's no salary data at all for that contract type that year), never an individual seniority inside it. salaryRangeCountcounts salary ranges, not offers, per seniority level. An offer declaring both a primary and a secondary range of the same contract type contributes twice, so this number can exceed the year's offer count for that seniority.topSkillsis never omitted, only empty, when a year has no skill data.quarterstotals are independent of the year's. Each quarter'scontractType.totalandseniority.totalare their own denominators, separate from the year-level totals and from every other quarter — don't sum quarters to get the year's numbers, read the year-level fields for that.salaryB2B/salaryUoPcan be omitted per-quarter the same way they can be omitted per-year, independently of whether the year-level object is present.
400 Bad Request — invalid or missing campaign:
Make sure that campaign parameter exists and contains only lowercase letters, numbers and dashes (max 64 chars long).
404 Not Found — unknown scopeKey (empty body).
429 Too Many Requests — rate limit exceeded. Retry after a short delay.
In the /examples directory you will find ready-to-run scripts showing how to integrate with the API. Each example works after cloning the repository — just navigate to the directory and run a single command.
| Language | Requirements | Directory | Command |
|---|---|---|---|
| JavaScript / Node.js | Node.js 18+ | examples/javascript |
node fetch_offers.mjs |
| C# / .NET | .NET 9 SDK | examples/csharp |
dotnet run |
| Python | Python 3.8+ | examples/python |
pip install -r requirements.txt && python fetch_offers.py |
| Go | Go 1.21+ | examples/go |
go run . |
| Java | Java 11+ | examples/java |
javac *.java && java FetchOffers |
| PHP | PHP 7.4+ | examples/php |
php fetch_offers.php |
| Ruby | Ruby 2.7+ | examples/ruby |
ruby fetch_offers.rb |
| Rust | Rust 1.70+ | examples/rust |
cargo run |
| Swift | Swift 5.9+ | examples/swift |
swift run |
Every language directory also ships a market-statistics example. It fetches all sections for the subcategory/React scope, prints every returned value, then makes a second call with fields=demand,salary to show the section filter in action.
| Language | Directory | Command |
|---|---|---|
| JavaScript / Node.js | examples/javascript |
node fetch_statistics.mjs |
| C# / .NET | examples/csharp |
dotnet run stats |
| Python | examples/python |
python fetch_statistics.py |
| Go | examples/go |
go run . stats |
| Java | examples/java |
javac *.java && java FetchStatistics |
| PHP | examples/php |
php fetch_statistics.php |
| Ruby | examples/ruby |
ruby fetch_statistics.rb |
| Rust | examples/rust |
cargo run -- stats |
| Swift | examples/swift |
swift run solidjobs-example stats |
Every language directory also ships a market-raport example. It fetches the yearly report for the ManualTester role and prints, for each year, the offer count, the contract-type and seniority splits, and the B2B / UoP salary levels.
| Language | Directory | Command |
|---|---|---|
| JavaScript / Node.js | examples/javascript |
node fetch_raport.mjs |
| C# / .NET | examples/csharp |
dotnet run raport |
| Python | examples/python |
python fetch_raport.py |
| Go | examples/go |
go run . raport |
| Java | examples/java |
javac *.java && java FetchRaport |
| PHP | examples/php |
php fetch_raport.php |
| Ruby | examples/ruby |
ruby fetch_raport.rb |
| Rust | examples/rust |
cargo run -- raport |
| Swift | examples/swift |
swift run solidjobs-example raport |
If you have an idea for extending the public API with new endpoints or found a bug in the documentation, please open an Issue.
This project is licensed under the MIT license.