{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/freehire-me/json-schema/freehire-me-job-schema.json",
  "title": "Job",
  "x-generated": "2026-11-09",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/_original/freehire-me-api-openapi.yml#/components/schemas/Job",
  "type": "object",
  "additionalProperties": true,
  "required": [
    "public_slug",
    "title",
    "company"
  ],
  "properties": {
    "public_slug": {
      "type": "string",
      "description": "Stable public identifier. The freehire page is https://freehire.me/jobs/{public_slug}."
    },
    "title": {
      "type": "string"
    },
    "company": {
      "type": "string"
    },
    "company_slug": {
      "type": "string"
    },
    "url": {
      "type": "string",
      "description": "The employer's own application URL. This is where an applicant goes.\nEmpty when `direct_apply` is true: that vacancy has no page elsewhere.\n"
    },
    "direct_apply": {
      "type": "boolean",
      "description": "True for a vacancy its employer published on freehire that takes\napplications only on its freehire page (`https://freehire.me/jobs/{slug}`).\nAbsent for every other posting.\n"
    },
    "location": {
      "type": "string",
      "description": "The posting's own location string, verbatim and unnormalized. Filter on the geography facets instead."
    },
    "source": {
      "type": "string",
      "description": "Origin slug — the ATS or board this posting was crawled from."
    },
    "external_id": {
      "type": "string",
      "description": "The source's own identifier for the posting."
    },
    "application_deadline": {
      "type": "string",
      "format": "date",
      "description": "Source-stated last application day, inclusive. Absent when the source states no deadline."
    },
    "manually_added": {
      "type": "boolean",
      "description": "True for a hand-curated posting rather than an automated crawl."
    },
    "description": {
      "type": "string",
      "description": "Stored verbatim HTML by default. `searchJobs` serves a truncated\npreview; `agentSearchJobs` and `getJob` serve the full body, and\n`agentSearchJobs` can convert it via `description_format`.\n"
    },
    "regions": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "countries": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "cities": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "metros": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "work_mode": {
      "type": "string",
      "enum": [
        "remote",
        "hybrid",
        "onsite"
      ],
      "description": "Absent when the posting's format could not be resolved."
    },
    "skills": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "collections": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Curated collection slugs of the posting's company."
    },
    "is_tech": {
      "type": "string",
      "enum": [
        "tech",
        "non_tech"
      ],
      "description": "Absent when unknown."
    },
    "requires_clearance": {
      "type": "boolean",
      "description": "Present and `true` only when the posting states a government\nsecurity-clearance requirement. Absent otherwise — there is no\n`false`, because \"no requirement was detected\" is not the same claim\nas \"this posting needs no clearance\".\n"
    },
    "via_recruiter": {
      "type": "boolean",
      "description": "Present and `true` only when the posting states that it is published\nby an intermediary on a client's behalf. Absent otherwise — a\nrecruiter who never says so is not detected, so absence does not\nmean the employer posted it.\n"
    },
    "equity": {
      "type": "string",
      "enum": [
        "salary_and_equity",
        "equity_only"
      ],
      "description": "What the posting states about equity compensation:\n`salary_and_equity` (offered beside cash pay) or `equity_only`\n(equity is the whole pay). Absent when the posting states neither —\n\"not stated\", not \"no equity\".\n"
    },
    "clearance_levels": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "confidential",
          "secret",
          "top_secret",
          "ts_sci",
          "public_trust",
          "interim",
          "other"
        ]
      },
      "description": "The security-clearance levels the posting states. Present exactly\nwhen `requires_clearance` is, absent otherwise. `other` is a non-US\nscheme or a clearance stated with no level.\n"
    },
    "perks": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Benefits the description states it offers (see the `perks` filter\nfor the vocabulary). Empty when it names none of them — not a claim\nthat the job offers none.\n"
    },
    "engagement": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "The legal terms the posting states it engages its worker on (see\nthe `engagement` filter for the vocabulary). Empty when it does not\nsay.\n"
    },
    "auto_apply_available": {
      "type": "boolean",
      "description": "Present and `true` only when the posting's ATS provider is one\nfreehire's auto-apply feature can currently attempt to fill and\nsubmit for. Absent otherwise — a best-effort provider-eligibility\nsignal, never a guarantee of successful submission.\n"
    },
    "apply_questions": {
      "type": "integer",
      "minimum": 0,
      "description": "How many questions the posting's application form REQUIRES beyond the\nstandard fields every form asks for (name, contact details, CV, cover\nletter), counted from the form freehire captured from the employer's\nATS. Optional questions, the equal-opportunity survey and consent boxes\nare not counted. `0` means no REQUIRED question beyond the basics\n(the form may still offer optional ones). Absent when freehire holds\nno form for the posting (none captured from its ATS, and none written\nfor a vacancy taking applications on freehire) — \"not known\", never\n\"long\".\nServed by search hits and the job detail; a list read straight from\nthe database may omit it.\n"
    },
    "posted_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time"
    },
    "created_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "When freehire first saw the posting."
    },
    "updated_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time"
    },
    "last_seen_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "When a re-crawl last confirmed the posting was still live."
    },
    "closed_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "Non-null once the posting is no longer open. Only `getJob` ever serves such a row."
    },
    "enrichment": {
      "$ref": "#/$defs/Enrichment"
    },
    "enriched_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time"
    },
    "enrichment_version": {
      "type": [
        "integer",
        "null"
      ],
      "description": "Bumped when the enrichment contract changes — use it to detect re-enriched rows."
    },
    "my_vote": {
      "type": [
        "integer",
        "null"
      ],
      "description": "The calling user's vote. Always null on this unauthenticated schema."
    },
    "reality": {
      "$ref": "#/$defs/Reality"
    },
    "ghost": {
      "$ref": "#/$defs/Ghost"
    },
    "view_count": {
      "type": "integer"
    },
    "applied_count": {
      "type": "integer"
    },
    "upvote_count": {
      "type": "integer"
    },
    "downvote_count": {
      "type": "integer"
    }
  },
  "$defs": {
    "Enrichment": {
      "type": "object",
      "additionalProperties": true,
      "description": "Model-extracted attributes, every field omitted when the posting did not\nstate it. `work_mode`, `regions`, `countries`, `cities` and `skills` are\ndeliberately NOT here — those are served top-level from the\ndeterministic dictionaries, and the model's own values for them are not\npublished.\n",
      "properties": {
        "summary": {
          "type": "string",
          "description": "The one synthesized field — a 1-2 sentence synopsis, always present on an enriched posting."
        },
        "employment_type": {
          "type": "string"
        },
        "relocation": {
          "type": "string"
        },
        "visa_sponsorship": {
          "type": "boolean"
        },
        "timezone_note": {
          "type": "string"
        },
        "salary_min": {
          "type": "integer"
        },
        "salary_max": {
          "type": "integer"
        },
        "salary_currency": {
          "type": "string"
        },
        "salary_period": {
          "type": "string"
        },
        "seniority": {
          "type": "string"
        },
        "experience_years_min": {
          "type": "integer"
        },
        "english_level": {
          "type": "string"
        },
        "education_level": {
          "type": "string"
        },
        "category": {
          "type": "string"
        },
        "domains": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "posting_language": {
          "type": "string"
        },
        "company_type": {
          "type": "string"
        },
        "company_size": {
          "type": "string"
        }
      }
    },
    "Ghost": {
      "type": [
        "object",
        "null"
      ],
      "additionalProperties": true,
      "description": "Hedged \"is anyone actually hiring for this\" verdict, raised only when\nevidence fires — most postings carry `null`, so an absent value means\n\"no signal\", never \"verified real\".\n\nAttached by `searchJobs`, `getJob` and the plain jobs list. NOT attached\nby `agentSearchJobs` or `getSimilarJobs`, so its absence on those two is\nnot evidence either way.\n",
      "properties": {
        "level": {
          "type": "string",
          "description": "How strongly the evidence reads, e.g. `possible`."
        },
        "criteria": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Which criteria fired, e.g. `evergreen_posting`, `ats_absent`."
        },
        "criteria_total": {
          "type": "integer",
          "description": "How many criteria exist, so `criteria` reads as N-of-total."
        },
        "contributors": {
          "type": "integer",
          "description": "Number of independent user reports, once past the disclosure gate."
        },
        "ats_checked_at": {
          "type": "string",
          "format": "date-time",
          "description": "When the posting was last looked for on the employer's own ATS. Present only when the `ats_absent` criterion fired."
        }
      }
    },
    "Reality": {
      "type": "object",
      "additionalProperties": true,
      "description": "Signals about whether the posting reflects a real, current opening.",
      "properties": {
        "class": {
          "type": "string",
          "enum": [
            "fresh",
            "stale",
            "likely-evergreen"
          ]
        },
        "age_days": {
          "type": "integer"
        },
        "repost_count": {
          "type": "integer",
          "description": "How many times this role has been reposted."
        },
        "mass_posting_count": {
          "type": "integer",
          "description": "How many near-identical postings the same role cluster holds."
        },
        "fake_freshness": {
          "type": "boolean",
          "description": "True when the stated posting date looks refreshed rather than real."
        }
      }
    }
  }
}