{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/cavuno-com/json-schema/cavuno-com-update-job-body-schema.json",
  "title": "UpdateJobBody",
  "x-generated": "2026-11-04",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/cavuno-com-openapi.json#/components/schemas/UpdateJobBody",
  "type": "object",
  "properties": {
    "companyId": {
      "type": "string",
      "description": "Identifier of the company the job belongs to."
    },
    "description": {
      "type": "string",
      "maxLength": 25000,
      "description": "Long-form description of the role, as HTML. Up to 25,000 characters. The HTML is sanitized on write: tags and attributes outside the supported formatting set (paragraphs, headings, lists, links, inline emphasis) are removed. Plain text without HTML tags is converted to paragraphs."
    },
    "slug": {
      "type": "string",
      "description": "URL-friendly slug for the job. Auto-generated from `title` when omitted."
    },
    "employmentType": {
      "type": "string",
      "enum": [
        "full_time",
        "part_time",
        "contract",
        "internship",
        "temporary",
        "volunteer",
        "other"
      ],
      "description": "Employment type of the role. When `customEmploymentType` is also sent, this must be the Google equivalent of that custom type (or be omitted: the server sets it). Sending a built-in type without `customEmploymentType` clears any custom type on the job."
    },
    "customEmploymentType": {
      "type": [
        "string",
        "null"
      ],
      "minLength": 1,
      "maxLength": 100,
      "description": "Key of one of the board's custom employment types (`GET /v1/settings/job-form` → `employmentType.customTypes`). The job is saved with the Google equivalent of that type as its built-in `employmentType`, which is what Google for Jobs and feeds see. An unknown key, a type the board no longer offers (`offered: false`), or an `employmentType` that differs from its Google equivalent returns `400 jobs_constraint_violation`. Pass `null` to clear the custom type and keep the built-in; omitted means unchanged."
    },
    "remoteOption": {
      "type": "string",
      "enum": [
        "on_site",
        "hybrid",
        "remote"
      ],
      "description": "Whether the role is on-site, hybrid, or fully remote. Omitted means unchanged; `null` returns `400` because a job's workplace type can be changed but not cleared. **Never valid on its own:** `on_site` and `hybrid` require at least one `officeLocations` entry, and `remote` requires `remotePermits` (use `[{\"type\":\"worldwide\",\"value\":\"worldwide\"}]` for anywhere; `remoteTimezones` then auto-derives on POST). Sending it alone returns `400`."
    },
    "remotePermits": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "worldwide",
              "world_region",
              "continent",
              "region",
              "subregion",
              "subdivision",
              "country",
              "custom"
            ]
          },
          "value": {
            "type": "string",
            "minLength": 1
          },
          "code": {
            "type": "string",
            "description": "Ignored on input. Job reads add it to some entries; send it back or leave it out."
          }
        },
        "required": [
          "type",
          "value"
        ],
        "additionalProperties": false
      },
      "description": "Where remote candidates must hold work authorization. Each entry is the smallest relevant scope: `worldwide`, a `world_region` (EMEA / LATAM / NA / APAC), a `continent`, a `region`, a `subregion`, a `custom` group (e.g. `EU`), a `country` (ISO 3166-1 alpha-2), or a `subdivision` (ISO 3166-2). Subdivisions auto-imply their parent country in the derived `remoteWorkPermitCountryCodes` output. Worldwide is mutually exclusive with all other entries. The canonical `{type, value}` set is published at `GET /v1/taxonomies/remote-permits`. Pass `[]` to clear an existing constraint."
    },
    "remoteTimezones": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "all",
              "world_region",
              "continent",
              "region",
              "subregion",
              "country",
              "timezone"
            ]
          },
          "value": {
            "type": "string",
            "minLength": 1
          },
          "plusMinus": {
            "type": "number",
            "minimum": 0,
            "maximum": 12
          }
        },
        "required": [
          "type",
          "value"
        ],
        "additionalProperties": false
      },
      "description": "Where remote candidates must be timezone-compatible. Each entry mirrors the `remotePermits` shape (`world_region`, `continent`, `region`, `subregion`, `country`) plus `timezone` (specific IANA name with optional `plusMinus` ±N hours expansion) and `all` (every timezone: equivalent of `worldwide` for permits). The canonical `{type, value}` set is published at `GET /v1/taxonomies/remote-timezones`. **When omitted on POST**, the server auto-derives this from `remotePermits` (or `[{all,all}]` if neither was provided). **PATCH never auto-re-derives**: once set, only an explicit replacement updates the stored value, even when `remotePermits` changes. Pass `[]` to clear an existing constraint."
    },
    "remoteSponsorship": {
      "type": "string",
      "enum": [
        "yes",
        "no",
        "unknown"
      ],
      "description": "Whether the employer sponsors visas for remote candidates. One of `yes`, `no`, or `unknown`."
    },
    "seniority": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "entry_level",
        "associate",
        "mid_level",
        "senior",
        "lead",
        "principal",
        "director",
        "executive"
      ],
      "description": "Seniority level of the role. Pass `null` to clear the stored value; omitted means unchanged."
    },
    "applicationUrl": {
      "type": "string",
      "minLength": 1,
      "maxLength": 2048,
      "description": "Where candidates apply. Accepts an HTTPS URL, a `mailto:` URI, or a bare email address (which is normalized to `mailto:` form)."
    },
    "salaryMin": {
      "type": [
        "number",
        "null"
      ],
      "minimum": 0,
      "description": "Minimum salary, in `salaryCurrency` units. Pass `null` to clear the stored value; omitted means unchanged. When a request sends any salary field, the resulting salary (sent values over stored ones) must have `salaryCurrency` and `salaryTimeframe` whenever `salaryMin` or `salaryMax` is set; otherwise `400`. Requests that send no salary field are not checked."
    },
    "salaryMax": {
      "type": [
        "number",
        "null"
      ],
      "minimum": 0,
      "description": "Maximum salary, in `salaryCurrency` units. Pass `null` to clear the stored value; omitted means unchanged. When a request sends any salary field, the resulting salary (sent values over stored ones) must have `salaryCurrency` and `salaryTimeframe` whenever `salaryMin` or `salaryMax` is set; otherwise `400`. Requests that send no salary field are not checked."
    },
    "salaryCurrency": {
      "type": [
        "string",
        "null"
      ],
      "description": "Three-letter ISO 4217 currency code for `salaryMin` and `salaryMax`. Pass `null` to clear the stored value; omitted means unchanged. When a request sends any salary field, the resulting salary (sent values over stored ones) must have `salaryCurrency` and `salaryTimeframe` whenever `salaryMin` or `salaryMax` is set; otherwise `400`. Requests that send no salary field are not checked."
    },
    "salaryTimeframe": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "per_year",
        "per_month",
        "per_week",
        "per_day",
        "per_hour"
      ],
      "description": "Period the `salaryMin` and `salaryMax` figures are quoted against. Pass `null` to clear the stored value; omitted means unchanged. When a request sends any salary field, the resulting salary (sent values over stored ones) must have `salaryCurrency` and `salaryTimeframe` whenever `salaryMin` or `salaryMax` is set; otherwise `400`. Requests that send no salary field are not checked."
    },
    "skills": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 100,
      "description": "Up to 100 canonical skill slugs from `GET /v1/taxonomies/skills`. Every slug must exist in this account."
    },
    "categories": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 100,
      "description": "Up to 100 canonical category slugs from `GET /v1/taxonomies/categories`. Every slug must exist in this account."
    },
    "isFeatured": {
      "type": "boolean",
      "description": "Whether the job appears in featured slots on the public board."
    },
    "expiresAt": {
      "anyOf": [
        {
          "type": "integer",
          "minimum": 0
        },
        {
          "nullable": true
        }
      ],
      "description": "Job expiry as a Unix epoch in milliseconds. On create, omitted or `null` defaults to 30 days from creation. On PATCH, `null` clears the explicit expiry; an ordinary published job then receives the board's `defaultJobDurationDays` from its publication time (30 days when unset). Draft and sponsored jobs may have no expiry. Past timestamps remove the job from the public board."
    },
    "publishedAt": {
      "type": "integer",
      "minimum": 0,
      "description": "Time at which the job was first published, as a Unix epoch in milliseconds. When omitted on create with `status: \"published\"`, the server stamps the current time. Useful for bulk-importing historical jobs while preserving original publication dates. PATCH may overwrite an existing value but cannot clear it."
    },
    "educationRequirements": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "high_school",
          "associate_degree",
          "bachelor_degree",
          "professional_certificate",
          "postgraduate_degree",
          "no_requirements"
        ]
      },
      "description": "Required education credentials. Each value is one of `high_school`, `associate_degree`, `bachelor_degree`, `professional_certificate`, `postgraduate_degree`, or `no_requirements`."
    },
    "experienceMonths": {
      "type": "integer",
      "minimum": 0,
      "description": "Minimum required experience, expressed in months."
    },
    "experienceInPlaceOfEducation": {
      "type": "boolean",
      "description": "If `true`, equivalent experience may substitute for the listed education requirements."
    },
    "inOfficePeriod": {
      "type": "string",
      "enum": [
        "per_week",
        "per_month",
        "per_year"
      ],
      "description": "Period denominator for `inOfficeFrequency`."
    },
    "inOfficeFrequency": {
      "type": "number",
      "minimum": 0,
      "description": "How often the candidate must be in-office over `inOfficePeriod`."
    },
    "officeLocations": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/JobOfficeLocationInput"
      },
      "description": "Physical office locations associated with the job. Prefer `{query: \"City, Country\"}` for free-form input; `{city, country, region?, locality?}` is also accepted when you already have structured fields. Each entry is resolved server-side; a country mismatch returns `400 jobs_unresolvable_location`."
    },
    "externalId": {
      "type": [
        "string",
        "null"
      ],
      "minLength": 1,
      "maxLength": 255,
      "description": "An external identifier for the job from your own system, such as an ATS requisition ID. Pass `null` to clear the stored value; omitted means unchanged."
    },
    "customFieldValues": {
      "type": "object",
      "additionalProperties": {
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          {
            "type": "boolean"
          },
          {
            "type": "number"
          },
          {
            "nullable": true
          }
        ]
      },
      "description": "Board-defined custom-field values, keyed by the field `key` (definitions, including type and option keys, are published at `GET /v1/settings/job-form`). Writes are **additive**: on `PATCH` a key you send is set/overwritten and a key you omit is preserved (unsent keys are never cleared); on `POST` this initializes the bag. Send a key with an intentional-empty value (`null`, `\"\"`, or `[]`) to **clear** it (`\"\"`/`null` clear any type; `[]` clears a `multi_select`); `false` and `0` are kept as real values. Values must match the field type and `single_select`/`multi_select` must use defined option **keys** (not labels); a wrong-typed value is rejected (`custom_field_wrong_type`), never silently cleared. Unknown keys are ignored. The stored bag never contains `null`/empty values."
    },
    "collectionValues": {
      "type": "object",
      "additionalProperties": {
        "anyOf": [
          {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 100
          },
          {
            "nullable": true
          }
        ]
      },
      "description": "Collection-reference selections keyed by the configured job collection field key. Values are arrays of record IDs, including for single-select fields. On PATCH omitted keys are preserved; send null or an empty array to clear a field. Keys for fields whose collection is archived are ignored, and their stored selections are kept."
    },
    "collectionOverrides": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/JobCollectionOverride"
      },
      "maxItems": 500,
      "description": "Per-job wording for selected collection entries, only on fields whose definition has `allowOverrides`. At most one item per selected entry; an item for an entry that is not selected on the job is rejected with `400 jobs_constraint_violation`. The list replaces the job's stored overrides: on PATCH omit it to keep them and send `[]` to clear them all. Deselecting an entry through `collectionValues` drops its override."
    },
    "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "The job title."
    }
  },
  "additionalProperties": false,
  "$defs": {
    "JobCollectionOverride": {
      "type": "object",
      "properties": {
        "fieldKey": {
          "type": "string",
          "minLength": 1,
          "description": "The collection field key used in `collectionValues`."
        },
        "recordId": {
          "type": "string",
          "minLength": 1,
          "description": "A record ID selected for that field on this job."
        },
        "title": {
          "type": "string",
          "maxLength": 160,
          "description": "The entry's title on this job, up to 160 characters. Omit or send a blank string to use the entry name."
        },
        "description": {
          "type": "string",
          "maxLength": 2000,
          "description": "The entry's description on this job, up to 2000 characters of plain text. When the default description field is rich text, the entry's `description` returns it escaped as HTML. Omit or send a blank string to use the default description."
        }
      },
      "required": [
        "fieldKey",
        "recordId"
      ]
    },
    "JobOfficeLocationInput": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "query": {
              "type": "string",
              "minLength": 1,
              "maxLength": 500,
              "description": "Free-form location string (e.g. `\"Berlin, Germany\"`, `\"Utrecht, Netherlands\"`, `\"Mountain View, California, USA\"`). Resolved server-side; rejected when no high-confidence match is found."
            }
          },
          "required": [
            "query"
          ],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": {
            "locality": {
              "type": "string",
              "minLength": 1,
              "description": "Neighborhood or sub-locality."
            },
            "city": {
              "type": "string",
              "minLength": 1,
              "description": "City."
            },
            "region": {
              "type": "string",
              "minLength": 1,
              "description": "Region, state, or province."
            },
            "country": {
              "type": "string",
              "minLength": 1,
              "description": "ISO 3166-1 alpha-2 country code OR recognized country name/alias. Aliases are normalized to canonical alpha-2 server-side (e.g. `US`, `USA`, `United States` → `US`; `UK`, `GB`, `United Kingdom` → `GB`)."
            }
          },
          "required": [
            "city",
            "country"
          ],
          "additionalProperties": false
        }
      ]
    }
  }
}