Jobs API
Every job posted in Azerbaijan, as JSON
Postings from company career pages and boards, deduplicated per source, with the employment type, job function and career level the source itself publishes, plus the full description on a detail fetch.
207K postings to query today, refreshed 1h ago, from 84 sources.
What the data is
Postings scraped from Azerbaijani job boards and company career pages, deduplicated per source, and served through the same envelope as every other dataset here. The card says “Refreshed every 15h” and that figure is the WORST gap between runs, not the average — a freshness claim is a worst case or it is nothing.
| Dataset | Sources | Refresh | Reference |
|---|---|---|---|
| Job Data API | 84 | Refreshed every 15h | Job Data docs |
The source count is the roster, not the yield: on a given run some sources fail or return nothing, and GET /v1/sources publishes when each one last delivered so you can see which is which.
Why you can trust it
Scraped data is only worth buying if you know where it is thin. Four things this API does about that, each specified somewhere you can check:
- The field list is generated from the serving code. Not written by hand — an earlier hand-written version documented seven job fields no source has ever produced. The published schema is what the docs render from, so a field described is a field the API returns.
- A value the source does not publish is
null. Never a placeholder, never a zero, and never inferred from prose. The city vocabulary is the clearest case: 99% of rows resolve, and the last 1% is a null rather than a guess. - Liveness is published with its limits attached.
active = trueis a default, andactive = falseis a finding — except where a source has stored nothing for 14 days and a daily pass retired what it held, which is no finding at all. The reference says so in those words. The reasoning is written up here, and the docs show how to derive something stricter. - Per-source quality is a response field.
GET /v1/sourcescarries aqualityobject — what share of a source's live rows have a description, a location, and a non-duplicate url. You can decide which sources are worth reading before you write the client.
How to start
Every response has the same three top-level keys, collections are cursor-paginated, and the key goes in an Authorization header. That is the whole contract; the envelope page specifies it once for every dataset.
curl -G https://api.softon.dev/v1/jobs \
-H "Authorization: Bearer $SOFTON_KEY" \
-d q=FINCA -d remote=false -d updated_after=2026-08-16T09:00:00Z
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.softon.dev/v1/jobs", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("SOFTON_KEY")) q := req.URL.Query() q.Set("q", "FINCA") q.Set("remote", "false") q.Set("updated_after", "2026-08-16T09:00:00Z") req.URL.RawQuery = q.Encode() res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var page struct { Data []Job `json:"data"` Meta struct{ Next *string `json:"next"` } `json:"meta"` } if err := json.NewDecoder(res.Body).Decode(&page); err != nil { log.Fatal(err) } // page.Meta.Next → send it back as ?cursor= for the following page
import json, os, urllib.parse, urllib.request url = "https://api.softon.dev/v1/jobs" + "?" + urllib.parse.urlencode({ "q": "FINCA", "remote": "false", "updated_after": "2026-08-16T09:00:00Z", }) req = urllib.request.Request(url, headers={ "Authorization": "Bearer " + os.environ["SOFTON_KEY"], }) page = json.load(urllib.request.urlopen(req)) # page["meta"]["next"] → send it back as ?cursor= for the following page
const url = new URL("https://api.softon.dev/v1/jobs"); url.searchParams.set("q", "FINCA"); url.searchParams.set("remote", "false"); url.searchParams.set("updated_after", "2026-08-16T09:00:00Z"); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SOFTON_KEY}` }, }); if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); const { data, meta } = await res.json(); // meta.next → send it back as ?cursor= for the following page
And what comes back, with a real posting from the dataset rather than one written for this page:
{
"data": [
{
"id": "glorri:fincaazerbaijan-neftcala-filiali-uzre-filial-kredit-satis-iscisi-72665882",
"source": "glorri",
"title": "Neftçala filialı üzrə Filial Kredit Satış İşçisi",
"company": { "name": "FINCA Azerbaijan", "slug": "fincaazerbaijan" },
"location": "Neftçala, Azərbaycan",
"remote": false,
"employment_type": "Full-time",
"job_function": "Bank services",
"career_level": "Professional",
"posted_at": "2026-10-05T09:39:25.578Z",
"url": "https://jobs.glorri.az/vacancies/fincaazerbaijan/fincaazerbaijan-neftcala-filiali-uzre-filial-kredit-satis-iscisi-72665882?isLocal=true",
"description": "FINCA Azerbaijan” Qeyri-Bank Kredit Təşkilatı Neftçala filialı üçün Filial Kredit Satış İşçisi vəzifəsinə vakansiya elan edir.…",
"requirements": "- Təhsil: Ali və ya Kollec təhsili;…",
"ingested_at": "2026-10-09T15:02:17.774673Z"
}
],
"meta": { "count": 1, "next": "eyJvIjoyfQ", "request_id": "req_9Fv3" },
"error": null
}
Read the quickstart for the three-minute version, or the jobs reference for the field table.
What it costs
Every account starts on the free plan: 100 requests a month at 5 req/s, no card. All live datasets are included. Paid plans are on the pricing table, and the rate-limit page explains both figures every response header carries.
What you may do with the rows — how long you may keep them, what you may redistribute, and the one thing we ask you to pass on — is on the acceptable-use page, written before the first paid customer rather than after.