---
title: "Create a job"
method: POST
path: "/jobs"
tags: ["Jobs"]
---

# Create a job

`POST /jobs`

## Request

- Base URL: `https://api.cavuno.com/v1`
- URL: `https://api.cavuno.com/v1/jobs`
- Auth: HTTP bearer

Creates a new job in `draft` status. The request is idempotent when an `Idempotency-Key` header is supplied. Reposts of existing jobs return 409 `jobs_already_exists` with the matching job ID. Always set `externalId` (the source job URL or ATS requisition ID) so reposts are detected reliably. When the 409 has `details.matched_on: "application_url"`, another job on your account uses the same `applicationUrl`; `details.matching_jobs` lists up to 5 of them. If the new job is a different role that shares that application destination (one ATS page or one inbox), retry with `allowSharedApplicationUrl: true`. The retry is a different request body, so send a new `Idempotency-Key` if you used one. `remoteOption` is required, and a `salaryMin` or `salaryMax` requires both `salaryCurrency` and `salaryTimeframe`; a missing one returns `400 validation_bad_request` naming the field.

## Request body

- CreateJobBody
  - `companyId` string: The ID of an existing company. **Required unless you supply `company`** — exactly one of the two must be present, and supplying both is an error.
  - `description` string, required: 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` string: URL-friendly slug for the job. Auto-generated from `title` when omitted.
  - `employmentType` 'full_time' | 'part_time' | 'contract' | 'internship' | 'temporary' | 'volunteer' | 'other': 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` string, nullable: 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` 'on_site' | 'hybrid' | 'remote', required: **Required.** Whether the role is on-site, hybrid, or fully remote: one of `on_site`, `hybrid`, `remote`. Omitting it or sending `null` returns `400`. It is not valid on its own either: `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).
  - `remotePermits` object[]: 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.
    - `type` 'worldwide' | 'world_region' | 'continent' | 'region' | 'subregion' | 'subdivision' | 'country' | 'custom', required
    - `value` string, required
  - `remoteTimezones` object[]: 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.
    - `type` 'all' | 'world_region' | 'continent' | 'region' | 'subregion' | 'country' | 'timezone', required
    - `value` string, required
    - `plusMinus` number
  - `remoteSponsorship` 'yes' | 'no' | 'unknown': Whether the employer sponsors visas for remote candidates. One of `yes`, `no`, or `unknown`.
  - `seniority` 'entry_level' | 'associate' | 'mid_level' | 'senior' | 'lead' | 'principal' | 'director' | 'executive': Seniority level of the role.
  - `applicationUrl` string, required: Where candidates apply. Accepts an HTTPS URL, a `mailto:` URI, or a bare email address (which is normalized to `mailto:` form).
  - `salaryMin` number, nullable: Minimum salary, in `salaryCurrency` units. `null` means not set. When `salaryMin` or `salaryMax` is set, `salaryCurrency` and `salaryTimeframe` are both required; otherwise `400`.
  - `salaryMax` number, nullable: Maximum salary, in `salaryCurrency` units. `null` means not set. When `salaryMin` or `salaryMax` is set, `salaryCurrency` and `salaryTimeframe` are both required; otherwise `400`.
  - `salaryCurrency` string, nullable: Three-letter ISO 4217 currency code for `salaryMin` and `salaryMax`. `null` means not set. Required when `salaryMin` or `salaryMax` is set; optional otherwise.
  - `salaryTimeframe` 'per_year' | 'per_month' | 'per_week' | 'per_day' | 'per_hour', nullable: Period the `salaryMin` and `salaryMax` figures are quoted against. `null` means not set. Required when `salaryMin` or `salaryMax` is set; optional otherwise.
  - `skills` string[]: Up to 100 canonical skill slugs from `GET /v1/taxonomies/skills`. Every slug must exist in this account.
  - `categories` string[]: Up to 100 canonical category slugs from `GET /v1/taxonomies/categories`. Every slug must exist in this account.
  - `isFeatured` boolean: Whether the job appears in featured slots on the public board.
  - `expiresAt` union: 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.
    - integer
    - unknown
  - `publishedAt` integer: 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` string[]: Required education credentials. Each value is one of `high_school`, `associate_degree`, `bachelor_degree`, `professional_certificate`, `postgraduate_degree`, or `no_requirements`.
  - `experienceMonths` integer: Minimum required experience, expressed in months.
  - `experienceInPlaceOfEducation` boolean: If `true`, equivalent experience may substitute for the listed education requirements.
  - `inOfficePeriod` 'per_week' | 'per_month' | 'per_year': Period denominator for `inOfficeFrequency`.
  - `inOfficeFrequency` number: How often the candidate must be in-office over `inOfficePeriod`.
  - `officeLocations` JobOfficeLocationInput[]: 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`.
    - union
      - object
        - `query` string, required: 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.
      - object
        - `locality` string: Neighborhood or sub-locality.
        - `city` string, required: City.
        - `region` string: Region, state, or province.
        - `country` string, required: 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`).
  - `externalId` string, nullable: An external identifier for the job from your own system, such as an ATS requisition ID. Use this value to look up the job later via `GET /v1/jobs?externalId=...` for deduplication. Always set it on create (the source job URL or ATS requisition ID) so reposts of the same job are detected reliably, even when several jobs share one `applicationUrl`. Scoped per-account: two different accounts may reuse the same `externalId` without collision. Up to 255 characters. `null` means not set.
  - `customFieldValues` object: 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` object: 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.
  - `collectionOverrides` JobCollectionOverride[]: 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.
    - `fieldKey` string, required: The collection field key used in `collectionValues`.
    - `recordId` string, required: A record ID selected for that field on this job.
    - `title` string: The entry's title on this job, up to 160 characters. Omit or send a blank string to use the entry name.
    - `description` string: 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.
  - `title` string, required: The job title.
  - `company` InlineCompanyInput: An inline company payload, resolved via `find-or-create` before the job is created. **Required unless you supply `companyId`** — exactly one of the two must be present, and supplying both is an error.
    - `name` string, required: The company's display name.
    - `website` string, nullable: Public company website URL. Used to resolve to an existing company by domain before falling back to creation. `null` or `""` means not set.
    - `summary` string, nullable: One-line summary of the company. Up to 280 characters. `null` or `""` means not set.
    - `matchByName` boolean: If `true` (default), falls back to matching by `name` when no website match is found. Set to `false` to match by website domain only.
    - `createIfMissing` boolean: If `true` (default), creates a new company when no match is found. Set to `false` to receive a `404 companies_not_found` response instead.
  - `status` 'draft' | 'published': Initial status of the job. Defaults to `draft`. Only `draft` and `published` are writable here. The system-set values `expired`, `archived`, and `pending_approval` are not. Use the dedicated transitions (`POST /v1/jobs/:id/publish`, `/pause`, `/expire`) for status changes after create.
  - `allowSharedApplicationUrl` boolean: Set to `true` to create this job even though another job on your account already uses the same `applicationUrl` (for example, several roles that share one ATS page or one application inbox). Only skips the application-URL duplicate check; a matching `externalId` or an identical job (same company, title, location and description) still returns 409 `jobs_already_exists`. Defaults to `false`.

## Response `201`

Successful response.

- Job
  - `id` string, required: Unique identifier for the object. Use this value as the `{id}` path parameter for the job endpoints (e.g. `GET /v1/jobs/{id}`).
  - `object` 'job', required: String representing the object's type. Objects of the same type share the same value.
  - `title` string, required: The job title.
  - `slug` string, nullable, required: URL-friendly slug used in public board URLs, or `null` if no slug is set.
  - `status` 'draft' | 'published' | 'expired' | 'archived' | 'pending_approval', required: Current status of the job. One of `draft`, `published`, `expired`, `archived`, or `pending_approval`. A `pending_approval` job is waiting for an operator to approve it: approve with `POST /v1/jobs/:id/publish`, reject with `DELETE /v1/jobs/:id`. It is not public, like a draft.
  - `companyId` string, nullable, required: Identifier of the company the job belongs to, or `null` if no company is attached.
  - `employmentType` 'full_time' | 'part_time' | 'contract' | 'internship' | 'temporary' | 'volunteer' | 'other', nullable, required: Employment type of the role, or `null` if not specified. For a job with a custom employment type, this is the Google equivalent of the custom type.
  - `customEmploymentType` object, nullable, required: The board's custom employment type for this job (for example `Casual`), or `null` when the job uses a built-in type only. A deleted custom type reads back as `null` and the job shows its built-in `employmentType`.
    - `key` string, required: Stable key of the custom employment type. Never changes.
    - `label` string, required: Display name of the custom employment type, as configured.
  - `remoteOption` 'on_site' | 'hybrid' | 'remote', nullable, required: Whether the role is on-site, hybrid, or fully remote. `null` only on older jobs saved without one; create requires it and `PATCH` cannot clear it.
  - `seniority` 'entry_level' | 'associate' | 'mid_level' | 'senior' | 'lead' | 'principal' | 'director' | 'executive', nullable, required: Seniority level of the role, or `null` if not specified.
  - `salaryMin` number, nullable, required: Minimum salary in `salaryCurrency` units, or `null` if not specified.
  - `salaryMax` number, nullable, required: Maximum salary in `salaryCurrency` units, or `null` if not specified.
  - `salaryCurrency` string, nullable, required: Three-letter ISO 4217 currency code for the salary range, or `null` if no salary is specified.
  - `salaryTimeframe` 'per_year' | 'per_month' | 'per_week' | 'per_day' | 'per_hour', nullable, required: Period the salary range is quoted against, or `null` if no salary is specified.
  - `isFeatured` boolean, required: Whether the job appears in featured slots on the public board.
  - `publishedAt` string, nullable, required: Time at which the job was first published, or `null` if not yet published. ISO 8601 datetime.
  - `expiresAt` string, nullable, required: Time at which the job expires as an ISO 8601 datetime. `null` can appear when the expiry requirement does not apply, including for drafts and sponsored jobs; ordinary published jobs always have an expiry.
  - `createdAt` string, required: Time at which the job was created. ISO 8601 datetime.
  - `updatedAt` string, required: Time at which the job was last updated. ISO 8601 datetime.
  - `revision` integer, required: Monotonic Job lifecycle revision used to reconcile webhook events. Legacy rows are 0 until the rollout backfill runs.
  - `skills` string[], required: Canonical skill slugs assigned to the job.
  - `categories` string[], required: Canonical category slugs assigned to the job.
  - `externalId` string, nullable, required: External identifier supplied by the caller on create, used for deduplication via `GET /v1/jobs?externalId=...`. `null` when no external identifier was set.
  - `customFieldValues` object, required: Board-defined custom-field values for this job, keyed by the field `key` (the definitions are published at `GET /v1/settings/job-form`). Each value is returned as stored: a string (`short_text` / `long_text` / a `single_select` option key), a string array (`multi_select` option keys), a boolean, or a number. Always an object: `{}` when the job has no custom-field values, never `null` or a missing field. Only real values are stored, so this never contains `null` or empty values.
  - `collectionValues` object, required: Operator-facing collection record IDs keyed by job collection field. Always an object; values are arrays even for single-select fields.
  - `links` object, required: Public + admin URLs for this job. `public` may be `null` if the job lacks a slug or company-slug.
    - `admin` string, uri, required: URL inside the Cavuno app where the authenticated owner can manage this resource.
    - `public` string, uri, nullable, required: Canonical public URL for this job, or `null` when the job has no slug or no associated company slug (so a stable URL cannot be assembled).
  - `resolvedCollectionFields` ResolvedJobCollectionField[], required: Collection selections resolved to current live names, logos, and public record details.
    - `key` string, required
    - `label` string, required
    - `entries` object[], required
      - `id` string, required
      - `name` string, required
      - `slug` string: URL-safe identifier, unique within the collection. Changing it keeps the previous value reserved.
      - `logoUrl` string: The entry's logo: the image in the collection's logo field, or a legacy logo URL on older entries. Absent when the entry has neither.
      - `title` string, required: The entry's title on this job: the job's `titleOverride` when set, otherwise the entry name. Render this.
      - `description` string, nullable, required: The entry's description on this job: the job's `descriptionOverride` when set, otherwise the value of the field's default description field, or `null` when neither exists. Sanitised HTML when the default description field is rich text, with a plain-text override escaped into paragraphs. Render this.
      - `titleOverride` string, nullable, required: The title this job set for the entry, or `null` when it uses the entry name.
      - `descriptionOverride` string, nullable, required: The plain-text description this job set for the entry, or `null` when it uses the default description.
      - `fields` object[], required
        - `key` string, required
        - `label` string, required
        - `type` 'short_text' | 'long_text' | 'rich_text' | 'single_select' | 'multi_select' | 'boolean' | 'number' | 'date' | 'url' | 'email' | 'phone' | 'image' | 'image_gallery' | 'file' | 'reference', required
        - `helpText` string
        - `options` object[]
          - `key` string, required
          - `label` string, required
      - `values` object, required
      - `references` object
  - `description` string, nullable, required: Long-form description of the role (HTML), or `null` if not specified.
  - `applicationUrl` string, nullable, required: Where candidates apply, or `null` if not specified. An HTTPS URL or `mailto:` URI.
  - `remotePermits` object[], required: Hierarchical permit selection authored by the caller. Lossless round-trip with the input: `[{type:"world_region",value:"EMEA"}]` goes in and comes out the same. The three `remoteWorkPermit*` and `remoteWorldwide` fields below are read-only derived projections of this list.
    - `type` string, required
    - `value` string, required
  - `remoteWorldwide` boolean, nullable, required: Read-only: derived from `remotePermits`. `true` only when the authored selection is a single `{type:"worldwide"}` entry.
  - `remoteTimezones` object[], required: Hierarchical timezone selection. Lossless round-trip with the authored input. 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. The flat `remoteAllowedTzOffsets` field below is the read-only derived projection used by search.
    - `type` string, required
    - `value` string, required
    - `plusMinus` number
  - `remoteAllowedTzOffsets` number[], required: Read-only: derived from `remoteTimezones` (per-country/per-region offset fan-out). UTC hour offsets used by the job-search index.
  - `remoteWorkPermitCountryCodes` string[], required: Read-only: derived from `remotePermits` (hierarchical groups fan out to alpha2 sets; subdivisions auto-add the parent country). ISO 3166-1 alpha-2 codes.
  - `remoteWorkPermitSubdivisionCodes` string[], required: Read-only: derived from `remotePermits` (subdivision entries only). ISO 3166-2 codes.
  - `remoteSponsorship` 'yes' | 'no' | 'unknown', required: Whether the employer sponsors visas for remote candidates. One of `yes`, `no`, or `unknown`.
  - `educationRequirements` string[], required: Required education credentials. Each value is one of `high_school`, `associate_degree`, `bachelor_degree`, `professional_certificate`, `postgraduate_degree`, or `no_requirements`.
  - `experienceMonths` number, nullable, required: Minimum required experience in months, or `null` if not specified.
  - `experienceInPlaceOfEducation` boolean, nullable, required: If `true`, equivalent experience may substitute for the listed education requirements. `null` if not specified.
  - `inOfficePeriod` 'per_week' | 'per_month' | 'per_year', nullable, required: Period denominator for `inOfficeFrequency`, or `null` if not specified.
  - `inOfficeFrequency` number, nullable, required: How often the candidate must be in-office over `inOfficePeriod`, or `null` if not specified.
  - `company` JobCompany, nullable, required: Embedded company resource for the job, or `null` if no company is attached.
    - `id` string, required: Unique identifier for the company.
    - `name` string, nullable, required: Display name of the company, or `null` if not yet set.
    - `slug` string, nullable, required: URL slug of the company, or `null` if not yet set.
    - `logoUrl` string, nullable, required: URL of the company logo, or `null` if no logo is set.
    - `website` string, nullable, required: Company website URL, or `null` if no website is set.
  - `officeLocations` JobOfficeLocation[], required: Physical office locations associated with the job.
    - `countryCode` string, nullable, required: ISO 3166-1 alpha-2 country code, or `null` if the location was not resolved.
    - `country` string, nullable, required: Full country name, or `null` if the location was not resolved.
    - `locality` string, nullable, required: Neighborhood or sub-locality, or `null` if not resolved.
    - `city` string, nullable, required: City, or `null` if not resolved.
    - `region` string, nullable, required: Region, state, or province name, or `null` if not resolved.
    - `regionCode` string, nullable, required: Region or state code (e.g. `CA` for California), or `null` if not resolved.
    - `postalCode` string, nullable, required: Postal or ZIP code, or `null` if not resolved.
    - `displayName` string, nullable, required: Pre-formatted display name for the location, or `null` if not resolved.

## Other responses

- `400`: The request was malformed.
- `404`: The supplied company does not exist or is not accessible.
- `409`: A job with the same `applicationUrl`, `externalId`, or content already exists on your account. The matching job ID is returned in `details.existing_job_id` and the check that matched in `details.matched_on` (`application_url`, `external_id`, or `composite_hash`). For `application_url`, `details.matching_jobs` lists up to 5 jobs that use the same application URL or email, each as `{ id, title, status }`; if this is a different role that shares that destination, retry with `allowSharedApplicationUrl: true` (and a new `Idempotency-Key` if you used one). A matching `externalId` or an identical job cannot be overridden.

## Changes

- **2026-10-19** `c34152c38176`: 3 info
  - added the optional property `error/upgrade` to the response with the `400` status
  - added the optional property `error/upgrade` to the response with the `404` status
  - added the optional property `error/upgrade` to the response with the `409` status

[Operation history](https://skmtc.dev/cavuno/apis/cavuno-v1-rest-api/changes/jobs/post.md)

---

[API](https://skmtc.dev/cavuno/apis/cavuno-v1-rest-api.md) · [All operations](https://skmtc.dev/cavuno/apis/cavuno-v1-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/cavuno/apis/cavuno-v1-rest-api/revisions/d5974dabff14?raw)
