> ## 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.

# Webhooks and data fetching

> Keep your copy of AI Apply data current with the data-changed webhook.

## Overview

AI Apply is asynchronous by design. Parsing a job posting and submitting an application both run in the background. AI Apply provides two webhooks so that you do not need to poll for results:

* The **data-changed webhook** notifies you that job postings in your environment changed. Fetch the changes with the `updated_after` filter to keep postings and application forms current.
* The **application updated webhook** reports the outcome of a single application and includes the application in its payload.

<Tip>
  **First time implementing Kombo webhooks?**

  View our general [Webhooks guide](/hris/guides/webhooks#validate-the-data) for delivery, signature validation, retries, and other behavior shared by every Kombo webhook.
</Tip>

## Data-changed webhook

### Strategy

Perform one full fetch of your job postings, then fetch only the changes whenever the data-changed webhook arrives.

```mermaid theme={null}
sequenceDiagram
    participant You
    participant Kombo
    Note over You,Kombo: Once
    You->>Kombo: Fetch all postings, page by page
    Kombo-->>You: Your full catalogue
    Note over You,Kombo: Continuously
    Kombo->>You: Data-changed webhook
    You->>Kombo: Fetch postings with updated_after
    Kombo-->>You: What changed since your last fetch
```

The webhook signals that data changed. The `updated_after` filter limits the response to changes since your previous fetch.

### The data-changed webhook

Kombo sends `ai-apply-data-changed` when one or more job postings in your environment changed in a customer-facing field: availability, archival, URL, job code, career site, or the application form. This includes a posting finishing parsing, a bulk import archiving postings, and a re-parse producing a new application form.

The payload names the changed model, not the changed records. The webhook is a notification, not a data delivery. Fetch the relevant endpoint after receiving it.

```json theme={null}
{
  "id": "2Cv6VeT4efBfzvQRudprNx5z",
  "type": "ai-apply-data-changed",
  "data": {
    "environment_id": "my-prod",
    "changed_models": [{ "name": "job_postings" }]
  }
}
```

The webhook is debounced with a 30-second window. The first change is delivered immediately. Changes within the following 30 seconds are batched into a single webhook, which starts the next window. You receive at most one webhook per 30 seconds, and none while no data changes.

<Note>
  `changed_models` covers job postings only. Application outcomes are not
  delivered through this webhook. They are delivered through the [application
  updated webhook](#application-updated-webhook), which includes the application
  itself, so no follow-up fetch is required.
</Note>

### Configuring the webhook

Create the webhook in the Kombo Dashboard under [Configuration, Webhooks](https://app.kombo.dev/configuration/webhooks). Select the type **AI Apply: Data changed**, enter a label, and enter the URL Kombo should call.

<Frame>
  <img
    src="https://mintcdn.com/kombo/a8ltGCXaK3FosIvX/images/ai-apply/create-data-changed-webhook.png?fit=max&auto=format&n=a8ltGCXaK3FosIvX&q=85&s=1bc3d23026849585c7fa11d629e812f2"
    alt="Creating the AI Apply data-changed
webhook"
    width="1974"
    height="1648"
    data-path="images/ai-apply/create-data-changed-webhook.png"
  />
</Frame>

Every webhook is signed with the signing secret shown on the same page. See [Validate the data](/hris/guides/webhooks#validate-the-data) for how to verify the signature.

### Fetching changes with updated\_after

Both job posting endpoints accept an `updated_after` query parameter in ISO 8601 format. The response includes every posting whose `updated_at` is equal to or later than the given timestamp.

Delta fetches are especially important for [GET Job Postings Forms](/ai-apply/v1/get-postings-forms). Application forms are large, so the endpoint returns at most five postings per page, and fetching an entire catalogue requires many requests. A delta after a webhook typically fits in one.

Which endpoint to sync depends on how you obtain application forms:

* If you [inquire per posting](/ai-apply/posting-inquiry) when a candidate applies, sync [GET Job Postings](/ai-apply/v1/get-postings). It provides posting metadata and availability without the form.
* If you [hold forms locally](/ai-apply/storing-application-forms), sync [GET Job Postings Forms](/ai-apply/v1/get-postings-forms). Each changed posting is returned with its current application form and submission token, so a delta fetch also delivers the replacement for any stored form.

Both guides document the response shapes in detail.

### Keeping track of the timestamp

Use the start time of your previous fetch as the `updated_after` checkpoint, not the latest `updated_at` value in its results. This is the same method used for the Unified API.

1. Record the current UTC time immediately before requesting the first page.
2. Follow `next` until it is `null`, keeping every filter unchanged between pages.
3. Once every page has succeeded, store the recorded start time as the new checkpoint.
4. On the next fetch, pass that checkpoint as `updated_after`.
5. Upsert results by posting `id`.

Recording the start time ensures that a posting changed while you were paging is included in the next delta. Advancing the checkpoint only after all pages succeed ensures that a failed fetch is repeated in full and no change is skipped.

Both properties, together with the inclusive comparison, mean that the same posting can be returned more than once. Upserting by posting `id` handles this without additional logic.

## Job Posting Updated Webhook

<Warning>
  The `ai-apply-job-posting-status-updated` webhook is deprecated. Use the
  [data-changed webhook](#the-data-changed-webhook) together with
  `updated_after` instead. It still fires for existing setups, but do not build
  on it.
</Warning>

### Lifecycle

| Status        | Emits webhook? | Description                                                                |
| ------------- | -------------- | -------------------------------------------------------------------------- |
| `PENDING`     | ❌              | Parsing is queued/processing                                               |
| `APPLYABLE`   | ✅              | Kombo has successfully parsed the job posting. You can submit applications |
| `UNAVAILABLE` | ✅              | Parsing the job posting failed                                             |
| `ARCHIVED`    | ✅              | The job was taken offline or manually archived                             |

<Tip>
  If a parse attempt fails but a previous successful revision still exists, the
  posting remains `APPLYABLE`.
</Tip>

### Payload

The payload matches the job posting schema you receive from the [get job posting endpoint](https://api.kombo.dev/docs/#/AI%20Apply/GetAiApplyPostings).

**Examples**

1. A job posting was successfully parsed.

```json theme={null}
{
  "id": "2Cv6VeT4efBfzvQRudprNx5z",
  "type": "ai-apply-job-posting-status-updated",
  "data": {
    "id": "9QGNv3B98kL3hyELE1qsZ86s",
    "career_site": { "id": "Chc4dua5asAQ48KUERDVF1bs", "label": "Acme" },
    "url": "https://careers.acme.com/jobs/fullstack-engineer-ai-infra-14102",
    "job_code": "ACME_13",
    "created_at": "2025-01-01T00:00:00.000Z",
    "updated_at": "2025-03-02T23:12:32.000Z",
    "archived_at": null,
    "archived_reason": null,
    "availability": "APPLYABLE"
  }
}
```

2. A job posting failed to parse.

```json theme={null}
{
  "id": "2Cv6VeT4efBfzvQRudprNx5z",
  "type": "ai-apply-job-posting-status-updated",
  "data": {
    "id": "9QGNv3B98kL3hyELE1qsZ86s",
    "career_site": { "id": "Chc4dua5asAQ48KUERDVF1bs", "label": "Acme" },
    "url": "https://careers.acme.com/jobs/fullstack-engineer-ai-infra-14102",
    "job_code": "ACME_13",
    "created_at": "2025-01-01T00:00:00.000Z",
    "updated_at": "2025-03-02T23:12:32.000Z",
    "archived_at": null,
    "archived_reason": null,
    "availability": "UNAVAILABLE"
  }
}
```

3. A job posting was taken offline.

```json theme={null}
{
  "id": "2Cv6VeT4efBfzvQRudprNx5z",
  "type": "ai-apply-job-posting-status-updated",
  "data": {
    "id": "9QGNv3B98kL3hyELE1qsZ86s",
    "career_site": { "id": "Chc4dua5asAQ48KUERDVF1bs", "label": "Acme" },
    "url": "https://careers.acme.com/jobs/fullstack-engineer-ai-infra-14102",
    "job_code": "ACME_13",
    "created_at": "2025-01-01T00:00:00.000Z",
    "updated_at": "2025-03-02T23:12:32.000Z",
    "archived_at": "2025-03-10T08:15:00.000Z",
    "archived_reason": "JOB_POSTING_TAKEN_OFFLINE",
    "availability": "ARCHIVED"
  }
}
```

### Handling job posting availability changes

A job posting's `availability` field may switch from being non-applyable to applyable, and vice versa.

As soon as this changes, you should update your UI accordingly.

We recommend either hiding the job from the list of jobs you display to candidates or showing the job URL directly for the candidate to apply on the career site.

<Info>
  Most transitions from being applyable to non-applyable are caused by a job
  posting being archived. In rare cases, a job posting's application form may
  change, triggering a re-parse.
</Info>

## Application Updated Webhook

### Lifecycle

| Status      | Emits webhook? | Description                                                                  |
| ----------- | -------------- | ---------------------------------------------------------------------------- |
| `PENDING`   | ❌              | Our automated system is applying or the application is pending manual review |
| `SUBMITTED` | ✅              | The application was successfully submitted                                   |
| `DUPLICATE` | ✅              | The ATS rejected the application because this candidate already applied      |
| `FAILED`    | ✅              | The application was deemed impossible to submit after human review           |

<Note>
  **When can an application be marked as `FAILED`?**

  If the candidate input is fundamentally invalid, and Kombo has no chance to
  submit the application in the candidate's name without the candidate providing
  more information, the application will be marked as failed. In the future, Kombo
  may support flows of following up with the candidate to collect remaining data
  in an automated manner.

  In rare edge cases, when a job is taken offline while an application is
  in-flight, the application will also be marked as failed.
</Note>

### Payload

The payload follows the application schema you receive from the [get application endpoint](https://api.kombo.dev/docs/#/AI%20Apply/GetAiApplyApplications).

**Examples**

1. Application is automatically submitted

```json theme={null}
{
  "id": "2Cv6VeT4efBfzvQRudprNx5z",
  "type": "ai-apply-application-status-updated",
  "data": {
    "id": "ADbmw5XSkeCSE1fAucoxEGnwZ",
    "job_posting_id": "JDn252PEYa4rMhKbJBjtn3ng",
    "status": "SUBMITTED",
    "candidate_email": "candidate@example.com",
    "proxy_email": "candidate@proxied.com",
    "created_at": "2025-01-01T00:00:00.000Z",
    "updated_at": "2025-03-02T23:12:32.000Z"
  }
}
```

2. Application is submitted after Kombo QA

```json theme={null}
{
  "id": "2Cv6VeT4efBfzvQRudprNx5z",
  "type": "ai-apply-application-status-updated",
  "data": {
    "id": "ADbmw5XSkeCSE1fAucoxEGnwZ",
    "job_posting_id": "JDn252PEYa4rMhKbJBjtn3ng",
    "status": "SUBMITTED",
    "candidate_email": "candidate@example.com",
    "proxy_email": null,
    "created_at": "2025-01-01T00:00:00.000Z",
    "updated_at": "2025-03-03T10:41:02.000Z"
  }
}
```

3. Application is marked as failed by Kombo QA (e.g., invalid candidate input)

```json theme={null}
{
  "id": "2Cv6VeT4efBfzvQRudprNx5z",
  "type": "ai-apply-application-status-updated",
  "data": {
    "id": "ADbmw5XSkeCSE1fAucoxEGnwZ",
    "job_posting_id": "JDn252PEYa4rMhKbJBjtn3ng",
    "status": "FAILED",
    "candidate_email": "candidate@example.com",
    "proxy_email": null,
    "created_at": "2025-01-01T00:00:00.000Z",
    "updated_at": "2025-03-10T08:15:00.000Z"
  }
}
```
