{
  "components": {
    "parameters": {
      "Category": {
        "description": "Comma-separated. uncategorised selects rows with no confident classification.",
        "in": "query",
        "name": "category",
        "schema": {
          "type": "string"
        }
      },
      "City": {
        "in": "query",
        "name": "city",
        "schema": {
          "type": "string"
        }
      },
      "Company": {
        "description": "Comma-separated company slugs.",
        "in": "query",
        "name": "company",
        "schema": {
          "type": "string"
        }
      },
      "CompanyDomain": {
        "description": "Comma-separated bare domains, e.g. `stripe.com,figma.com` — no scheme\nand no path. The join key you already have in a CRM. 37.2% of\ncompanies carry a domain; the rest can never match this filter.\n",
        "in": "query",
        "name": "company_domain",
        "schema": {
          "type": "string"
        }
      },
      "Country": {
        "description": "Comma-separated ISO-3166 alpha-2.",
        "in": "query",
        "name": "country",
        "schema": {
          "type": "string"
        }
      },
      "DescriptionContains": {
        "description": "Full-text over the advert body. Only ever matches rows that have one,\nso it implies `has_description=true`.\n",
        "in": "query",
        "name": "description_contains",
        "schema": {
          "type": "string"
        }
      },
      "EmploymentType": {
        "description": "Comma-separated; `not_stated` selects rows with none.",
        "in": "query",
        "name": "employment_type",
        "schema": {
          "type": "string"
        }
      },
      "ExcludeCategory": {
        "in": "query",
        "name": "exclude_category",
        "schema": {
          "type": "string"
        }
      },
      "ExcludeCompanyDomain": {
        "description": "Same vocabulary as company_domain, removed instead of kept.",
        "in": "query",
        "name": "exclude_company_domain",
        "schema": {
          "type": "string"
        }
      },
      "ExcludeCountry": {
        "in": "query",
        "name": "exclude_country",
        "schema": {
          "type": "string"
        }
      },
      "ExcludeProvider": {
        "description": "Same vocabulary as `provider`, removed instead of kept.",
        "in": "query",
        "name": "exclude_provider",
        "schema": {
          "type": "string"
        }
      },
      "ExcludeSourceType": {
        "in": "query",
        "name": "exclude_source_type",
        "schema": {
          "type": "string"
        }
      },
      "HasDescription": {
        "description": "`true` returns only the 842,414 live rows (38.2%) that carry a description.",
        "in": "query",
        "name": "has_description",
        "schema": {
          "type": "boolean"
        }
      },
      "HasSalary": {
        "description": "`true` (and `structured`) returns only rows whose salary the SOURCE published — the meaning this parameter has always had, kept so that adding derived salaries does not change the results of a query you already ship. `any` also includes figures we read out of the advert text (`salary_source: parsed_description`, reported as `inferred`). AI estimates are never published under any value.\n",
        "in": "query",
        "name": "has_salary",
        "schema": {
          "enum": [
            "true",
            "structured",
            "any"
          ],
          "type": "string"
        }
      },
      "IncludeDescription": {
        "description": "Return the full advert text in a `description` field. Off by default:\ndescriptions average 2,581 bytes, so a 200-row page would be a 516 KB\nresponse nobody asked for. With this on, `limit` may not exceed 50 —\na larger request is refused with 422 rather than quietly clamped.\n",
        "in": "query",
        "name": "include_description",
        "schema": {
          "type": "boolean"
        }
      },
      "MaxSalary": {
        "description": "Upper bound on salary_min_annual_eur. Same 2.0% caveat as min_salary.",
        "in": "query",
        "name": "max_salary",
        "schema": {
          "type": "number"
        }
      },
      "MinSalary": {
        "description": "Lower bound on `salary_min_annual_eur`. Selects ONLY rows with\nstructured salary we could normalise — 2.0% of the ledger — so this\nis a narrow filter by nature, not a broken one.\n",
        "in": "query",
        "name": "min_salary",
        "schema": {
          "type": "number"
        }
      },
      "PostedAfter": {
        "description": "Date or RFC3339 timestamp.",
        "in": "query",
        "name": "posted_after",
        "schema": {
          "type": "string"
        }
      },
      "Provider": {
        "description": "Comma-separated list of the exact source systems to include, e.g.\n`greenhouse,lever,workday`. This is the ATS or board a vacancy came\nfrom, finer than source_type which buckets them. A name we do not\npublish is a 422, never a silently empty page. Up to 12 values.\n",
        "in": "query",
        "name": "provider",
        "schema": {
          "type": "string"
        }
      },
      "Q": {
        "description": "Full-text over title, company name and location — NOT the description.\nThat is deliberate, not a limitation: use `description_contains` for\nthe advert body. Stemming is off (`simple` dictionary), so `q=engineer`\ndoes not match `engineering`.\n",
        "in": "query",
        "name": "q",
        "schema": {
          "type": "string"
        }
      },
      "Remote": {
        "description": "`remote`, `hybrid`, `on_site`, or `not_stated`.",
        "in": "query",
        "name": "remote",
        "schema": {
          "type": "string"
        }
      },
      "RemoteConfirmed": {
        "description": "`true` returns only listings whose remote status the SOURCE stated —\n187,485 of 1,970,675 live rows (9.5%). Without it you also receive\nthe 688,407 (34.9%) where we inferred it from the location text, the\ntitle, or the presence of a named workplace city.\n",
        "in": "query",
        "name": "remote_confirmed",
        "schema": {
          "type": "boolean"
        }
      },
      "Seniority": {
        "description": "Comma-separated; `not_stated` selects rows with none.",
        "in": "query",
        "name": "seniority",
        "schema": {
          "type": "string"
        }
      },
      "SourceType": {
        "description": "Comma-separated provenance filter. The legacy spellings\n`employer_ats`, `government` and `direct` are still accepted and map\nonto `ats`, `public_agency` and `career_site`.\n\nFilter values are matched case-insensitively: `category=engineering`\nand `category=Engineering` are the same query. Enumerate the legal\nvalues with /v1/meta/facets.\n",
        "in": "query",
        "name": "source_type",
        "schema": {
          "examples": [
            "ats",
            "career_site,public_agency"
          ],
          "type": "string"
        }
      },
      "Title": {
        "description": "Full-text over the job title only. Combines with the others as an AND:\n`?title=engineer\u0026description_contains=kubernetes`.\n",
        "in": "query",
        "name": "title",
        "schema": {
          "type": "string"
        }
      },
      "TitleExclude": {
        "description": "Drop rows whose title matches these words. Same matching as `title`.",
        "in": "query",
        "name": "title_exclude",
        "schema": {
          "type": "string"
        }
      },
      "VerifiedAfter": {
        "description": "Only rows re-confirmed at their source since this instant.",
        "in": "query",
        "name": "verified_after",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "401": {
        "$ref": "#/components/responses/Unauthorized"
      },
      "429": {
        "$ref": "#/components/responses/RateLimited"
      },
      "NotFound": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "description": "No such row."
      },
      "PlanUpgradeRequired": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "description": "This endpoint is not included in your plan. `X-JOA-Required-Feature` names the missing entitlement — `delta_feed` or `bulk_export`. See https://jobopportunitiesapi.org/api for the tiers that include it.\n"
      },
      "RateLimited": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "description": "Over your plan's per-minute or per-day limit. `Retry-After` says how long."
      },
      "RecordQuotaExhausted": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "description": "The plan's monthly RECORD allowance is spent. Distinct from 429: a 429 means slow down and retry, a 402 means the licence is used up until the 1st of the month. Do not retry. Every metered response carries `X-RateLimit-Records-Remaining`, so this is visible long before it happens.\n"
      },
      "Unauthorized": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "description": "Missing, unknown, revoked or expired key. All four are reported identically."
      },
      "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.\n"
      }
    },
    "schemas": {
      "Company": {
        "properties": {
          "careers_url": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "first_seen": {
            "type": "string"
          },
          "industry": {
            "type": "string"
          },
          "logo": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "open_roles": {
            "description": "Roles retrievable from THIS API. Never a number the API cannot honour.",
            "type": "integer"
          },
          "org_type": {
            "type": "string"
          },
          "own_site_roles": {
            "description": "Vacancies seen on the company own careers page.",
            "type": "integer"
          },
          "sectors": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "slug": {
            "type": "string"
          },
          "source_types": {
            "description": "`aggregator` and `agency` are reserved and match no rows today; see the source_type enum.\n",
            "items": {
              "enum": [
                "ats",
                "career_site",
                "public_agency",
                "aggregator",
                "agency"
              ],
              "type": "string"
            },
            "type": "array"
          },
          "website": {
            "type": "string"
          },
          "website_verified": {
            "description": "When we fetched this domain and found the company own name on it.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "CompanyPage": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/Company"
            },
            "type": "array"
          },
          "has_more": {
            "type": "boolean"
          },
          "limit": {
            "type": "integer"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "offset": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "Error": {
        "properties": {
          "docs": {
            "type": "string"
          },
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "FieldSources": {
        "description": "Per-field provenance. `published` means the value was carried by the\nsource. `inferred` means WE produced it — it may be right, it is not a\nquotation. `absent` means no value, which is a different failure from a\nguess and is reported as such.\n\n`category` and `seniority` are ALWAYS `inferred`: both are read off the\njob title by a classifier, never from a field the employer filled in.\n\n`salary` is `published` when the source stated it in a field\n(`salary_source: structured`), and `inferred` when we read it out of the\nadvert text (`salary_source: parsed_description`). It is never an\nestimate: the projection refuses to emit Erioun's AI salary predictions\nat all, so a modelled figure cannot reach this API by any route.\n",
        "properties": {
          "category": {
            "enum": [
              "published",
              "inferred",
              "absent"
            ],
            "type": "string"
          },
          "description": {
            "enum": [
              "published",
              "inferred",
              "absent"
            ],
            "type": "string"
          },
          "employment_type": {
            "enum": [
              "published",
              "inferred",
              "absent"
            ],
            "type": "string"
          },
          "location": {
            "enum": [
              "published",
              "inferred",
              "absent"
            ],
            "type": "string"
          },
          "posted_at": {
            "enum": [
              "published",
              "inferred",
              "absent"
            ],
            "type": "string"
          },
          "remote": {
            "enum": [
              "published",
              "inferred",
              "absent"
            ],
            "type": "string"
          },
          "salary": {
            "enum": [
              "published",
              "inferred",
              "absent"
            ],
            "type": "string"
          },
          "seniority": {
            "enum": [
              "published",
              "inferred",
              "absent"
            ],
            "type": "string"
          },
          "source_type": {
            "description": "`inferred` when the value is the provider's default class;\n`published` when the row itself was classified upstream.\n\nTODAY THIS IS ALWAYS `inferred`: the row-level column is null on\nevery live listing, so the provider's class is the only path taken.\n`published` is reserved for when per-row classification lands. Do not\nbuild a filter that expects it to appear.\n",
            "enum": [
              "published",
              "inferred",
              "absent"
            ],
            "type": "string"
          }
        },
        "required": [
          "remote",
          "employment_type",
          "category",
          "seniority",
          "salary",
          "location",
          "posted_at",
          "description",
          "source_type"
        ],
        "type": "object"
      },
      "Job": {
        "properties": {
          "apply_url": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "category_confidence": {
            "description": "Classifier confidence in `category`. Below 0.6 no category is published at all, so this is always null or \u003e= 0.6.",
            "type": [
              "number",
              "null"
            ]
          },
          "city": {
            "type": "string"
          },
          "closed_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "closed_reason": {
            "enum": [
              "expired_upstream",
              "not_seen",
              null
            ],
            "type": [
              "string",
              "null"
            ]
          },
          "company": {
            "type": "string"
          },
          "company_logo": {
            "type": "string"
          },
          "company_slug": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "description": {
            "description": "The full advert text. Present ONLY when the request set include_description=true, and only on the 32.3% of rows that have one.\n",
            "type": "string"
          },
          "employment_type": {
            "type": "string"
          },
          "field_sources": {
            "$ref": "#/components/schemas/FieldSources"
          },
          "first_seen_at": {
            "description": "When the vacancy first entered this ledger. Populated on 100% of rows, unlike posted_at which the source often omits — so this is the field to sort or backfill by when you need every row to have a date.\n",
            "type": "string"
          },
          "has_description": {
            "type": "boolean"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "last_verified_at": {
            "description": "When we last confirmed this vacancy still exists at its source.",
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "posted_at": {
            "type": "string"
          },
          "provider_type": {
            "description": "The provider-level class, in the original vocabulary. Kept so a query written against it still resolves.",
            "enum": [
              "employer_ats",
              "government",
              "direct",
              "aggregator"
            ],
            "type": "string"
          },
          "remote": {
            "enum": [
              "remote",
              "hybrid",
              "on_site"
            ],
            "type": "string"
          },
          "remote_inferred": {
            "description": "**Always present.** True when we derived `remote` rather than read\nit. It used to be omitted when false, which made a stated value and\nan absent one identical on the wire.\n",
            "type": "boolean"
          },
          "salary_currency": {
            "type": "string"
          },
          "salary_max": {
            "type": "number"
          },
          "salary_min": {
            "type": "number"
          },
          "salary_min_annual_eur": {
            "description": "salary_min converted to an annual EUR figure, so a row quoting USD/hour and one quoting GBP/year are comparable. NULL when the period or the currency is unrecognised — never guessed, because reading an hourly rate as a salary is wrong by a factor of 2080. Converted with the indicative rates in /v1/meta/freshness, not a settlement rate.\n",
            "type": "number"
          },
          "salary_period": {
            "type": "string"
          },
          "salary_source": {
            "description": "Where the figure came from. `structured` is a field the source itself published. `parsed_description` is a real figure quoted in the advert text that WE read out and normalised — we chose the number and the period, so `field_sources.salary` reports it as `inferred`, never `published`.\n",
            "enum": [
              "structured",
              "parsed_description"
            ],
            "type": "string"
          },
          "seniority": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "source": {
            "description": "The provider id, e.g. greenhouse, company_site, eures.",
            "type": "string"
          },
          "source_type": {
            "description": "Per-row provenance where the source has been classified per row,\notherwise the provider's class. Live distribution: ats 1,063,129,\ncareer_site 672,526, public_agency 320,727.\n\nRESERVED VALUES: `aggregator` and `agency` are accepted by the filter\nbut currently match no rows. Every aggregator source is marked\nnon-redistributable, so that inventory never enters the ledger at\nall, and no provider is classified `agency` yet. Filtering on either\nreturns an empty page — that is the data, not a fault.\n",
            "enum": [
              "ats",
              "career_site",
              "public_agency",
              "aggregator",
              "agency",
              "unknown"
            ],
            "type": "string"
          },
          "status": {
            "enum": [
              "live",
              "closed"
            ],
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "upstream_expired_at": {
            "description": "When the radar proved the vacancy dead, as opposed to when we removed it.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "slug",
          "title",
          "company",
          "company_slug",
          "remote_inferred",
          "last_verified_at",
          "status",
          "source",
          "source_type",
          "provider_type",
          "has_description",
          "field_sources"
        ],
        "type": "object"
      },
      "JobPage": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/Job"
            },
            "type": "array"
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "type": "object"
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "description": "`Authorization: Bearer YOUR_API_SECRET`",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "description": "Every listing here comes from an employer ATS, a company's own careers page,\na public employment agency, or a direct submission. Redistribution is\nenforced by a database JOIN, not a filter someone can forget.\n\nThree things this API does that a job feed normally does not:\n\n* **It says which fields it guessed.** Every job carries `field_sources`,\n  naming each field as `published` (carried by the source), `inferred`\n  (derived by us) or `absent`. 36.3% of live listings have an *inferred*\n  remote status; 8.4% have one the source actually stated. Filter with\n  `remote_confirmed=true` to get only the latter.\n* **It says when each row was last verified.** `last_verified_at` is when we\n  last confirmed the vacancy still exists at its source, not when it was\n  posted. `GET /v1/meta/freshness` publishes the coverage numbers.\n* **It keeps what closed.** `GET /v1/jobs/closed` returns roles that have\n  come off their source, with the reason and the date. A req that opened\n  and closed in nine days is a hiring signal, not a dead link.\n\nCounts in this document are read from the live ledger when you fetch it, not\ntyped in: 2,205,150 live listings and 198,890 employers as of\n2026-08-05.\n",
    "summary": "A verified ledger of open jobs, with per-field provenance and closure history.",
    "title": "Job Opportunities API",
    "version": "2026-08-03"
  },
  "openapi": "3.1.0",
  "paths": {
    "/v1/changes": {
      "get": {
        "description": "Poll this instead of re-reading the ledger. Pass `since` on the first\ncall and the returned `next_since` from then on; it is a keyset cursor\nof `\u003ctimestamp\u003e|\u003cuuid\u003e`, so no row is lost to a shared timestamp.\n",
        "parameters": [
          {
            "in": "query",
            "name": "since",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 500,
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "count": {
                      "type": "integer"
                    },
                    "data": {
                      "items": {
                        "properties": {
                          "change": {
                            "enum": [
                              "created",
                              "updated",
                              "delisted"
                            ],
                            "type": "string"
                          },
                          "job": {
                            "$ref": "#/components/schemas/Job"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "next_since": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "A page of changes."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/RecordQuotaExhausted"
          },
          "403": {
            "$ref": "#/components/responses/PlanUpgradeRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "summary": "Delta feed — created, updated and delisted, in change order.",
        "tags": [
          "jobs"
        ]
      }
    },
    "/v1/companies": {
      "get": {
        "description": "Ordered by `GREATEST(open_roles, own_site_roles) DESC, name, slug`. The\nslug makes the ordering total: without it 18,213 tie groups covering\n36,464 of 186,558 companies (19.5%) could be reordered between two\nrequests, so offset paging silently repeated and skipped rows.\n\n`offset` is capped at 100,000 and an offset beyond that is a **422**, not\na silently clamped 200. Use `cursor` for deep paging — it is lossless and\nhas no limit.\n",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 25,
              "maximum": 200,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "maximum": 100000,
              "minimum": 0,
              "type": "integer"
            }
          },
          {
            "description": "The next_cursor from the previous page. Overrides offset.",
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "country",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "org_type",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "source_type",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Only companies whose domain we fetched and found their own name on.\nAn absent website means we could not prove one, not that they have none.\n",
            "in": "query",
            "name": "has_website",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "in": "query",
            "name": "q",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyPage"
                }
              }
            },
            "description": "A page of companies."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/RecordQuotaExhausted"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "summary": "Employers with hiring evidence.",
        "tags": [
          "companies"
        ]
      }
    },
    "/v1/companies/{slug}": {
      "get": {
        "parameters": [
          {
            "in": "path",
            "name": "slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Company"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The company."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/RecordQuotaExhausted"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "summary": "One employer.",
        "tags": [
          "companies"
        ]
      }
    },
    "/v1/export": {
      "get": {
        "description": "One JSON object per line, not a JSON array: the stream can be processed as it arrives, and a dropped connection costs one line rather than the whole document. Requires a plan with `bulk_export`.\nRows are ordered by `id`, so an interrupted transfer resumes exactly where it stopped — pass the last id you received as `after`. On a multi-gigabyte export that is not a rare need.\nRecords are charged in batches as they are written, not at the end, so an export that is abandoned partway is still billed for what it delivered. If the monthly allowance runs out mid-stream, the last line of the body is an object carrying `error: record_quota_exhausted` and the `after` cursor to resume from. A consumer must therefore check the final line rather than assume a clean end of stream.\nEvery filter from `GET /v1/jobs` applies.\n",
        "parameters": [
          {
            "description": "Resume cursor — the `id` of the last row you received.",
            "in": "query",
            "name": "after",
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "default": "live",
              "enum": [
                "live",
                "closed",
                "any"
              ],
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeDescription"
          },
          {
            "$ref": "#/components/parameters/Category"
          },
          {
            "$ref": "#/components/parameters/Country"
          },
          {
            "$ref": "#/components/parameters/Remote"
          },
          {
            "$ref": "#/components/parameters/Seniority"
          },
          {
            "$ref": "#/components/parameters/Provider"
          },
          {
            "$ref": "#/components/parameters/SourceType"
          },
          {
            "$ref": "#/components/parameters/Company"
          },
          {
            "$ref": "#/components/parameters/PostedAfter"
          },
          {
            "$ref": "#/components/parameters/VerifiedAfter"
          },
          {
            "$ref": "#/components/parameters/HasSalary"
          },
          {
            "$ref": "#/components/parameters/HasDescription"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            },
            "description": "A stream of listings, one JSON object per line."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/RecordQuotaExhausted"
          },
          "403": {
            "$ref": "#/components/responses/PlanUpgradeRequired"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "summary": "Bulk export of the full corpus, as a stream of NDJSON.",
        "tags": [
          "jobs"
        ]
      }
    },
    "/v1/jobs": {
      "get": {
        "description": "Keyset pagination on `(posted_at DESC NULLS LAST, id DESC)`. Pass the\n`next_cursor` from each response back as `cursor`. Offsets are not\noffered here: at this scale they double-count when rows shift.\n",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 25,
              "maximum": 200,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The next_cursor from the previous page.",
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "`live` (default) returns open vacancies. `closed` returns roles that\nhave left their source. `any` returns both; every row carries\n`status`. Paid endpoints only.\n",
            "in": "query",
            "name": "status",
            "schema": {
              "default": "live",
              "enum": [
                "live",
                "closed",
                "any"
              ],
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Category"
          },
          {
            "$ref": "#/components/parameters/Country"
          },
          {
            "$ref": "#/components/parameters/City"
          },
          {
            "$ref": "#/components/parameters/Remote"
          },
          {
            "$ref": "#/components/parameters/EmploymentType"
          },
          {
            "$ref": "#/components/parameters/Seniority"
          },
          {
            "$ref": "#/components/parameters/Provider"
          },
          {
            "$ref": "#/components/parameters/ExcludeProvider"
          },
          {
            "$ref": "#/components/parameters/SourceType"
          },
          {
            "$ref": "#/components/parameters/ExcludeSourceType"
          },
          {
            "$ref": "#/components/parameters/Company"
          },
          {
            "$ref": "#/components/parameters/ExcludeCategory"
          },
          {
            "$ref": "#/components/parameters/ExcludeCountry"
          },
          {
            "$ref": "#/components/parameters/RemoteConfirmed"
          },
          {
            "$ref": "#/components/parameters/HasSalary"
          },
          {
            "$ref": "#/components/parameters/HasDescription"
          },
          {
            "$ref": "#/components/parameters/IncludeDescription"
          },
          {
            "$ref": "#/components/parameters/PostedAfter"
          },
          {
            "$ref": "#/components/parameters/VerifiedAfter"
          },
          {
            "$ref": "#/components/parameters/Q"
          },
          {
            "$ref": "#/components/parameters/Title"
          },
          {
            "$ref": "#/components/parameters/TitleExclude"
          },
          {
            "$ref": "#/components/parameters/DescriptionContains"
          },
          {
            "$ref": "#/components/parameters/CompanyDomain"
          },
          {
            "$ref": "#/components/parameters/ExcludeCompanyDomain"
          },
          {
            "$ref": "#/components/parameters/MinSalary"
          },
          {
            "$ref": "#/components/parameters/MaxSalary"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobPage"
                }
              }
            },
            "description": "A page of listings."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/RecordQuotaExhausted"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "summary": "List listings, newest first.",
        "tags": [
          "jobs"
        ]
      }
    },
    "/v1/jobs/closed": {
      "get": {
        "description": "Keyset pagination on `(closed_at DESC, id DESC)`.\n\n`closed_reason` is not decoration. `expired_upstream` means the radar\nproved the vacancy dead — two misses at least an hour apart, re-checked\nagainst the board listing or the job URL itself. `not_seen` means it\nsimply stopped appearing at its source for the grace period, which is\nusually a closure and occasionally a board that broke.\n\nEvery filter from `GET /v1/jobs` also applies here.\n",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 25,
              "maximum": 200,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "closed_after",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "closed_before",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "closed_reason",
            "schema": {
              "enum": [
                "expired_upstream",
                "not_seen"
              ],
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Category"
          },
          {
            "$ref": "#/components/parameters/Country"
          },
          {
            "$ref": "#/components/parameters/City"
          },
          {
            "$ref": "#/components/parameters/Remote"
          },
          {
            "$ref": "#/components/parameters/EmploymentType"
          },
          {
            "$ref": "#/components/parameters/Seniority"
          },
          {
            "$ref": "#/components/parameters/Provider"
          },
          {
            "$ref": "#/components/parameters/ExcludeProvider"
          },
          {
            "$ref": "#/components/parameters/SourceType"
          },
          {
            "$ref": "#/components/parameters/ExcludeSourceType"
          },
          {
            "$ref": "#/components/parameters/Company"
          },
          {
            "$ref": "#/components/parameters/ExcludeCategory"
          },
          {
            "$ref": "#/components/parameters/ExcludeCountry"
          },
          {
            "$ref": "#/components/parameters/RemoteConfirmed"
          },
          {
            "$ref": "#/components/parameters/HasSalary"
          },
          {
            "$ref": "#/components/parameters/HasDescription"
          },
          {
            "$ref": "#/components/parameters/IncludeDescription"
          },
          {
            "$ref": "#/components/parameters/PostedAfter"
          },
          {
            "$ref": "#/components/parameters/VerifiedAfter"
          },
          {
            "$ref": "#/components/parameters/Q"
          },
          {
            "$ref": "#/components/parameters/Title"
          },
          {
            "$ref": "#/components/parameters/TitleExclude"
          },
          {
            "$ref": "#/components/parameters/DescriptionContains"
          },
          {
            "$ref": "#/components/parameters/CompanyDomain"
          },
          {
            "$ref": "#/components/parameters/ExcludeCompanyDomain"
          },
          {
            "$ref": "#/components/parameters/MinSalary"
          },
          {
            "$ref": "#/components/parameters/MaxSalary"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobPage"
                }
              }
            },
            "description": "A page of closed listings."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/RecordQuotaExhausted"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "summary": "Roles that have left their source, most recently closed first.",
        "tags": [
          "jobs"
        ]
      }
    },
    "/v1/jobs/expired": {
      "get": {
        "description": "The cheap half of /v1/jobs/closed. Returns id, closed_at and closed_reason only — enough to mark rows stale in your own database without transferring the jobs themselves. About 89% smaller than the same page from /v1/jobs/closed.\nKeyset paged exactly like /v1/changes: pass `since` on the first call and the returned `next_since` from then on. An empty page echoes your cursor back, so the loop keeps working once you are current.\n",
        "parameters": [
          {
            "description": "RFC3339 timestamp, or a `next_since` cursor from a previous call.",
            "in": "query",
            "name": "since",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "1-10000, default 1000.",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 1000,
              "maximum": 10000,
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "count": {
                      "type": "integer"
                    },
                    "data": {
                      "items": {
                        "properties": {
                          "closed_at": {
                            "format": "date-time",
                            "type": "string"
                          },
                          "closed_reason": {
                            "enum": [
                              "expired_upstream",
                              "not_seen"
                            ],
                            "type": "string"
                          },
                          "id": {
                            "format": "uuid",
                            "type": "string"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "next_since": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "A page of closures."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanUpgradeRequired"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "summary": "Ids of roles that have come off the ledger"
      }
    },
    "/v1/jobs/{id}": {
      "get": {
        "parameters": [
          {
            "description": "The uuid or the public slug.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Return the row even if it has been delisted. It carries `status=\"closed\"`.",
            "in": "query",
            "name": "include_closed",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Job"
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The listing."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/RecordQuotaExhausted"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "summary": "One listing, with its description.",
        "tags": [
          "jobs"
        ]
      }
    },
    "/v1/me": {
      "get": {
        "responses": {
          "200": {
            "description": "Key status."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "summary": "Your key, plan and quota.",
        "tags": [
          "meta"
        ]
      }
    },
    "/v1/meta/facets": {
      "get": {
        "description": "Facet keys: `family`, `country`, `city`, `remote`, `employment`,\n`seniority`, `source_type`, `remote_source`, and the legacy `source`.\n`remote_source` counts the provenance split itself — how much of the\nledger's remote status is stated and how much is our inference.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "data": {
                      "additionalProperties": {
                        "items": {
                          "properties": {
                            "count": {
                              "type": "integer"
                            },
                            "label": {
                              "type": "string"
                            },
                            "value": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "type": "array"
                      },
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Facet counts."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "summary": "Every filter value with its live count.",
        "tags": [
          "meta"
        ]
      }
    },
    "/v1/meta/freshness": {
      "get": {
        "description": "The claim the subscription is sold on, made checkable. Every live\nlisting is re-confirmed at its source on a rolling cycle; this reports\nthe coverage, plus per-field provenance totals so a buyer can size the\nhonest subset before they pay for it.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "closures": {
                          "properties": {
                            "last_24h": {
                              "type": "integer"
                            },
                            "last_7d": {
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        },
                        "field_coverage": {
                          "properties": {
                            "description": {
                              "type": "integer"
                            },
                            "employment_type": {
                              "type": "integer"
                            },
                            "remote_inferred": {
                              "type": "integer"
                            },
                            "remote_inferred_pct": {
                              "type": "number"
                            },
                            "remote_published": {
                              "type": "integer"
                            },
                            "remote_published_pct": {
                              "type": "number"
                            },
                            "salary_published": {
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        },
                        "generated_at": {
                          "type": "string"
                        },
                        "live_listings": {
                          "type": "integer"
                        },
                        "verification": {
                          "properties": {
                            "newest_verification": {
                              "type": "string"
                            },
                            "oldest_verification": {
                              "type": "string"
                            },
                            "reverified_last_24h": {
                              "type": "integer"
                            },
                            "reverified_last_24h_pct": {
                              "type": "number"
                            },
                            "reverified_last_48h": {
                              "type": "integer"
                            },
                            "reverified_last_48h_pct": {
                              "type": "number"
                            },
                            "reverified_last_7d": {
                              "type": "integer"
                            },
                            "reverified_last_hour": {
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Freshness and coverage report."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "summary": "How recently the ledger was verified, and how much of it is inferred.",
        "tags": [
          "meta"
        ]
      }
    }
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "servers": [
    {
      "url": "https://api.jobopportunitiesapi.org"
    }
  ],
  "tags": [
    {
      "name": "jobs"
    },
    {
      "name": "companies"
    },
    {
      "name": "meta"
    }
  ]
}