> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kombo.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Manual management of job postings

> Create career sites and job postings one at a time through the API, and what that costs you.

## Overview

Every job posting in AI Apply belongs to a career site, and both can be created one at a time through the API instead of through a [job feed](/ai-apply/job-feeds). This requires you to manage career site creation and updates to job postings as well.

**What you send:** one request per career site, then one request per job posting, each carrying a URL and the career site it belongs to.

**What you get:** the same job postings a bulk import would produce, parsed and ready to receive applications. Everything after creation is identical: you read postings, request the form, and submit applications the same way.

<Warning>
  **We recommend job feeds instead.** Creating postings by hand is more work to
  build and leaves you without a way to take postings down. See below before you
  choose this path.
</Warning>

### Why we advise against it

* **You own the bookkeeping.** Career site labels are not unique, so sending the same label twice creates a second career site rather than returning the first. Job feeds handle this out of the box.
* **You handle duplicates.** A job posting is identified by its career site, URL, and job code. Submitting a posting you already created returns an `AI_APPLY.JOB_POSTING_ALREADY_EXISTS` error. So you need to keep track of what you already sent to AI Apply and what still needs parsing.

## How it works

```mermaid theme={null}
sequenceDiagram
    participant You
    participant Kombo
    You->>Kombo: POST career site
    Kombo-->>You: Career site id
    You->>Kombo: POST job posting (url + career site id)
    Kombo-->>You: Job posting id, availability PENDING
    Kombo->>Kombo: Parse the posting in the background
    Kombo->>You: AI Apply data-changed webhook
```

The posting comes back before parsing has finished, with `availability` set to `PENDING`. It becomes `APPLYABLE` once parsing succeeds, and `UNAVAILABLE` if it fails.

We send an AI Apply data-changed webhook when a job posting's status changes. See [Data fetching](/ai-apply/webhooks#data-changed-webhook) for the lifecycle and payloads, or poll [GET Job Postings](/ai-apply/v1/get-postings) if you would rather not receive webhooks.

## Creating a career site

A career site groups job postings under one entity. Create one per employer whose jobs you send us.

[POST Career Sites](/ai-apply/v1/post-career-sites) takes a label and returns the career site:

```json theme={null}
{
  "id": "mK7pQw9xNvEr2LdY5sGh8TcZ",
  "label": "Acme Inc. California"
}
```

Store the returned `id`. You need it for every posting you create, and there is no way to look a career site up by label: [GET Career Sites](/ai-apply/v1/get-career-sites) lists them, but nothing stops two of them sharing a label.

<Tip>
  Use a label you can trace back to the source, and keep your own mapping from
  that source to the returned `id`. Recreating a career site because the mapping
  was lost splits one employer's postings across two entities.
</Tip>

## Creating a job posting

[POST Job Postings](/ai-apply/v1/post-postings) queues a posting for parsing:

```json theme={null}
{
  "career_site_id": "mK7pQw9xNvEr2LdY5sGh8TcZ",
  "url": "https://careers.acme.com/jobs/fullstack-engineer-14102",
  "job_code": "ACME_13",
  "location": {
    "country": "US",
    "postal_code": "94116"
  }
}
```

| Field            | Required | Behavior                                                                                                                                                            |
| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `career_site_id` | Yes      | The career site to group this posting under.                                                                                                                        |
| `url`            | Yes      | The job posting we parse and later submit applications to.                                                                                                          |
| `job_code`       | No       | Your own identifier for the job. Also part of what makes a posting unique.                                                                                          |
| `location`       | No       | Where we apply from. We route the browser session through that country, and through that state for US postal codes. Without it, we run from our own infrastructure. |

<Note>
  General considerations for job postings (query parameters, location, etc.)
  still apply. See the [AI Apply
  documentation](/ai-apply/introduction#parsing-a-job-posting) for details.
</Note>

The response is the job posting, with `availability` set to `PENDING` until parsing finishes:

```json theme={null}
{
  "id": "9QGNv3B98kL3hyELE1qsZ86s",
  "career_site": {
    "id": "mK7pQw9xNvEr2LdY5sGh8TcZ",
    "label": "Acme Inc. California"
  },
  "url": "https://careers.acme.com/jobs/fullstack-engineer-14102",
  "job_code": "ACME_13",
  "availability": "PENDING",
  "archived_at": null,
  "archived_reason": null
}
```

Submitting the same career site, URL, and job code again returns an error rather than a second posting:

```json theme={null}
{
  "status": "error",
  "error": {
    "code": "AI_APPLY.JOB_POSTING_ALREADY_EXISTS",
    "title": "A job posting with this URL already exists for this career site.",
    "message": "A job posting with the same URL has already been submitted for this career site. You can retrieve the existing job posting via GET /v1/ai-apply/postings and filtering by URL.",
    "log_url": "https://app.kombo.dev/my-prod/logs?interactionId=123456"
  }
}
```

## Keeping state in sync

Postings you create this way are archived in two cases: when we detect that the job has been taken offline, and when someone archives it in the Kombo Dashboard. Neither transition is dispatched from your system, so you learn about it rather than cause it.

Keep your copy current by syncing [GET Job Postings](/ai-apply/v1/get-postings) with `updated_after`, triggered by the data-changed webhook or on a schedule. The pattern is described in [Data fetching](/ai-apply/webhooks#data-changed-webhook). Before you show a job to a candidate, check `availability` and `archived_at` on your copy. Anything you no longer offer that is still `APPLYABLE` on our side is yours to hide. [Job feeds](/ai-apply/job-feeds) remove this reconciliation entirely: one request replaces the whole list and archives whatever you left out.

## API reference

* [POST Career Sites](/ai-apply/v1/post-career-sites)
* [GET Career Sites](/ai-apply/v1/get-career-sites)
* [POST Job Postings](/ai-apply/v1/post-postings)
* [GET Job Postings](/ai-apply/v1/get-postings)
