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.
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 | 97 | Refreshed every 15h | Job Data docs |
| Event Data API | 2 | Refreshed daily | Event Data docs |
| Quotes Data API | 1 | One-time import | Quotes Data docs |
| Rates Data API | 1 | Refreshed daily | Rates Data docs |
| Genome Source Releases API | 1 | Refreshed daily | Genome Source Releases docs |
| Reachability API | 1 | Refreshed daily | Reachability 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 = falseis a finding;active = trueis a default, and 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=golang -d remote=true -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", "golang") q.Set("remote", "true") 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": "golang", "remote": "true", "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", "golang"); url.searchParams.set("remote", "true"); 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
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: 1,000 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.