jobopportunitiesapi-org Public API

Keyless, aggregate-only routes under /public/ (no rows, statistics only). Most of /public/* mirrors a keyed /v1 endpoint and is documented there; the two BETA history routes below have no keyed twin and are documented here in full. The rest of /public/* is documented at https://jobopportunitiesapi.org/docs/endpoints/public.

Operations 3

GET /public/companies/{slug}/open-roles-history BETA — partial data. #
GET /public/market-signals/history BETA — partial data. #
GET /public/site-stats The site's headline numbers, one name and one definition each (daily snapshot) #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/jobopportunitiesapi-org:jobopportunitiesapi-org-public-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

jobopportunitiesapi-org-public-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  description: 'Every listing here comes from an employer ATS, a company''s own careers page,

    a public employment agency, or a direct submission. Redistribution is

    enforced by a database JOIN, not a filter someone can forget.


    Three things this API does that a job feed normally does not:


    * **It says which fields it guessed.** Every job carries `field_sources`,

    naming each field as `published` (carried by the source), `inferred`

    (derived by us) or `absent`. 88.0% of live listings have an *inferred*

    remote status; 11.3% have one the source actually stated. Filter with

    `remote_confirmed=true` to get only the latter.

    * **It says when each row was last verified.** `last_verified_at` is when we

    last confirmed the vacancy still exists at its source, not when it was

    posted. `GET /v1/meta/freshness` publishes the coverage numbers.

    * **It keeps what closed.** `GET /v1/jobs/closed` returns roles that have

    come off their source, with the reason and the date. A req that opened

    and closed in nine days is a hiring signal, not a dead link. The date is

    the day JOA''s daily verification confirmed the advert had gone; before

    late September 2026 that verification ran in large catch-up batches, so

    earlier closure dates show when JOA confirmed a closure, not when the

    employer closed the role.


    Changelog 2026-10-07: new contract fields `engagement`,

    `contract_category`, `agency` and `working_schedule`, each with

    `field_sources` provenance and filters (`?engagement=`,

    `?contract_category=`, `?agency=`, `?working_schedule=`; absence token

    `unknown`, alias `not_stated`). Values appear only when the posting or the

    source states them (or, later, when an audited rule establishes them) —

    most postings will show `unknown` at launch, and that is the honest answer,

    not a defect. `contract_category=permanent` returns only postings that

    STATE permanence. Per-field coverage is published on /public/coverage.

    `employment_type` is unchanged and frozen.


    Counts in this document are not typed in: they come from the daily

    statistics snapshot every page of the website uses (GET /public/site-stats):

    2,080,732 live employer-direct listings (employer_direct_live) and

    129,261 employers as of 2026-10-20.'
  summary: A verified ledger of open jobs, with per-field provenance and closure history.
  title: Job Opportunities Public API
  version: '2026-10-07'
servers:
- url: https://api.jobopportunitiesapi.org
security:
- bearerAuth: []
tags:
- description: 'Keyless, aggregate-only routes under /public/ (no rows, statistics only).

    Most of /public/* mirrors a keyed /v1 endpoint and is documented there;

    the two BETA history routes below have no keyed twin and are documented

    here in full. The rest of /public/* is documented at

    https://jobopportunitiesapi.org/docs/endpoints/public.'
  name: Public
paths:
  /public/companies/{slug}/open-roles-history:
    get:
      description: 'Keyless. Reads a snapshot of joa.companies.open_roles (already maintained by

        refresh_companies()), taken by joa.snapshot_company_open_history() into

        joa.company_open_history — never a job row, never a salary or time-to-fill figure.

        Snapshots are daily from 2026-10-06. Before that only two snapshots exist

        (2026-09-28 and 2026-10-04), so older windows have gaps; nothing is reconstructed

        at present. Each point carries a "method": "snapshot" (a count recorded that day) or

        "reconstructed" (a value derived afterwards from the ledger; a reconstructed point never

        replaces a snapshot). An unknown slug returns 200 with an empty points array rather

        than 404. Edge-cached >=1h.'
      parameters:
      - in: path
        name: slug
        required: true
        schema:
          type: string
      - description: Window size. Defaults to 30.
        in: query
        name: days
        schema:
          enum:
          - 30
          - 90
          - 365
          type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    properties:
                      beta:
                        type: boolean
                      days_requested:
                        type: integer
                      history_starts:
                        format: date
                        nullable: true
                        type: string
                      note:
                        type: string
                      partial_data:
                        type: boolean
                      points:
                        items:
                          properties:
                            date:
                              format: date
                              type: string
                            method:
                              description: How the point was obtained.
                              enum:
                              - snapshot
                              - reconstructed
                              type: string
                            open_roles:
                              type: integer
                          type: object
                        type: array
                      slug:
                        type: string
                    type: object
                type: object
          description: A daily open-roles trend for the named company.
        '422':
          $ref: '#/components/responses/Unprocessable'
      security: []
      summary: BETA — partial data.
      tags:
      - Public
      operationId: getPublicCompaniesBySlugOpenRolesHistory
      x-operation-id-source: derived
  /public/market-signals/history:
    get:
      description: 'Keyless. Reads a once-a-day snapshot of the same rollup GET /public/market-signals

        reads live (joa.market_signal_segments), taken by joa.snapshot_market_signal_history()

        (db/193) into joa.market_signal_history. History begins the day the daily snapshot

        first ran — there is no backfill — and every response carries "beta": true,

        "partial_data": true and "history_starts" so a caller cannot mistake a short series

        for a long one. Same restriction as the live endpoint: family/country/seniority only,

        no title, no company. Edge-cached >=1h.'
      parameters:
      - description: One value of the family facet of /public/facets.
        in: query
        name: family
        schema:
          type: string
      - description: ISO 3166-1 alpha-2.
        in: query
        name: country
        schema:
          type: string
      - description: One value of the seniority facet of /public/facets.
        in: query
        name: seniority
        schema:
          type: string
      - description: Window size. Defaults to 30.
        in: query
        name: days
        schema:
          enum:
          - 30
          - 90
          - 365
          type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    properties:
                      beta:
                        type: boolean
                      days_requested:
                        type: integer
                      history_starts:
                        description: Earliest date any snapshot exists, across every segment. Null before the first snapshot has ever run.
                        format: date
                        nullable: true
                        type: string
                      note:
                        type: string
                      partial_data:
                        type: boolean
                      points:
                        items:
                          properties:
                            closed:
                              type: integer
                            companies:
                              type: integer
                            date:
                              format: date
                              type: string
                            listings:
                              type: integer
                            open:
                              type: integer
                            state:
                              enum:
                              - published
                              - suppressed
                              type: string
                          type: object
                        type: array
                    type: object
                type: object
          description: A daily trend for the requested segment, oldest first.
        '422':
          $ref: '#/components/responses/Unprocessable'
      security: []
      summary: BETA — partial data.
      tags:
      - Public
      operationId: getPublicMarketSignalsHistory
      x-operation-id-source: derived
  /public/site-stats:
    get:
      description: 'Keyless. The single source of every headline number on

        jobopportunitiesapi.org, /public/stats, /public/coverage, llms.txt and

        apis.json. Taken once a day (normally ~05:00 UTC, from a coverage report measured minutes earlier); `as_of` names the day.

        Every surface shows the same values for the same `as_of`.


        `metrics.employer_direct_live` is THE live count: adverts a caller can

        obtain from /v1/jobs. `live_including_withheld` and

        `employers_including_withheld` include adverts JOA holds back and are

        never the number JOA sells. `closed_with_closure_date` carries the

        closure-date caveat in `closure_date_note`. Regional totals state their

        country sets in `regions`.'
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  as_of:
                    format: date
                    type: string
                  closure_date_note:
                    type: string
                  definitions:
                    additionalProperties:
                      type: string
                    type: object
                  frozen_at:
                    format: date-time
                    type: string
                  measured_at:
                    format: date-time
                    type: string
                  metrics:
                    properties:
                      closed_with_closure_date:
                        type: integer
                      countries_with_live_roles:
                        type: integer
                      country_unknown_live:
                        type:
                        - integer
                        - 'null'
                      employer_direct_live:
                        type: integer
                      employers:
                        type: integer
                      employers_including_withheld:
                        type:
                        - integer
                        - 'null'
                      europe_live:
                        type: integer
                      listings_ledger_rows:
                        type: integer
                      live_including_withheld:
                        type: integer
                      north_america_live:
                        type: integer
                      posted_last_7d:
                        type: integer
                      withheld_listings:
                        type: integer
                    type: object
                  regions:
                    type: object
                  stale:
                    type: boolean
                type: object
          description: The daily snapshot.
        '503':
          description: No snapshot has been taken yet.
      security: []
      summary: The site's headline numbers, one name and one definition each (daily snapshot)
      tags:
      - Public
      operationId: getPublicSiteStats
      x-operation-id-source: derived
components:
  schemas:
    Error:
      properties:
        docs:
          type: string
        error:
          type: string
        message:
          type: string
      type: object
  responses:
    Unprocessable:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
      description: 'A parameter we will not guess at: an unknown source_type or provider, an unparseable timestamp, a malformed cursor. Note the deliberate exceptions, which are clamps rather than errors: `limit` is capped at the plan maximum (a non-numeric value falls back to the default), and comma-separated lists are truncated to the per-parameter maximum shown on each. Unknown query parameters are ignored.

        '
  securitySchemes:
    bearerAuth:
      description: '`Authorization: Bearer YOUR_API_SECRET`'
      scheme: bearer
      type: http