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

# Async Endpoints

> How to work with asynchronous endpoints that process requests in the background and return results via polling.

Some Kombo endpoints process requests **asynchronously**. Instead of
returning the final result in the HTTP response, they return a `task_id`
that you use to poll for the result. This is used for operations that may
take longer to complete – such as bulk writes to a connected system.

The following endpoints in this category use the async pattern:

* [Upsert skills](/hris/v1/post-skills-bulk)
  * [Get status of “Upsert skills” task](/hris/v1/get-skills-bulk-task-id)

## How it works

Working with an async endpoint is a three-step process:

1. **Submit** – Send a `POST` request with your payload. You receive a
   `task_id` immediately.
2. **Poll** – Call the corresponding `GET` endpoint with the `task_id` at
   regular intervals.
3. **Collect the result** – Once the status is `COMPLETED` or `FAILED`, the
   response contains the final result or error details.

```mermaid theme={null}
sequenceDiagram
    participant You
    participant Kombo
    participant Tool as Connected Tool

    You->>Kombo: POST /endpoint (payload)
    Kombo-->>You: 200 { task_id }

    loop Poll until terminal status
        You->>Kombo: GET /endpoint/{task_id}
        Kombo-->>You: { status: "PENDING" }
    end

    Note over Kombo,Tool: Processing in background

    You->>Kombo: GET /endpoint/{task_id}
    Kombo-->>You: { status: "COMPLETED", data: [...] }
```

## Task statuses

Every task is in exactly one of three statuses:

| Status      | Meaning                                                                         | Terminal? |
| ----------- | ------------------------------------------------------------------------------- | --------- |
| `PENDING`   | The task is still being processed. Keep polling.                                | No        |
| `COMPLETED` | The task finished processing all items. The `data` field contains the results.  | Yes       |
| `FAILED`    | The task could not be processed at all. The `error` field contains the details. | Yes       |

<Note>
  `COMPLETED` means Kombo finished processing the task – it does **not**
  mean every item in the request succeeded. Individual items may have
  failed (e.g., one course could not be created in the connected tool).
  These per-item outcomes are reported in the `data` array, where each
  entry has its own `status` of `SUCCEEDED` or `FAILED`.

  In contrast, a task-level `FAILED` status means the task could not run
  at all – for example, because the credentials for the connected tool
  are no longer valid. In that case, no items were processed.

  In some cases, the connected tool supports batch processing, which
  Kombo uses internally. When that happens, multiple items may share the
  same outcome because they were processed as a single batch. Such
  scenarios are usually explained in the endpoint-specific docs or listed
  as action constraints in the
  [coverage grid](/the-dashboard#tool-coverage).
</Note>

## Submitting a request

Send a `POST` request just like any other action endpoint. The response
contains a `task_id` that identifies the background task.

The following example uses the [LMS bulk upsert courses
endpoint](/lms/v1/post-courses-bulk) for illustration, but the pattern is
the same for all async endpoints:

```bash theme={null}
curl --request POST \
  --url 'https://api.kombo.dev/v1/lms/courses/bulk' \
  --header 'Authorization: Bearer <api_key>' \
  --header 'X-Integration-Id: <integration_id>' \
  --header 'Content-Type: application/json' \
  --data '{
    "items": [
      {
        "origin_id": "cybersecurity-101",
        "course": {
          "type": "EXTERNAL",
          "title": "Introduction to Cybersecurity",
          "description": "Learn the fundamentals of cybersecurity.",
          "course_url": "https://your-platform.com/courses/cybersecurity-101"
        }
      },
      {
        "origin_id": "data-privacy-basics",
        "course": {
          "type": "EXTERNAL",
          "title": "Data Privacy Basics",
          "description": "Understand GDPR and data privacy best practices.",
          "course_url": "https://your-platform.com/courses/data-privacy-basics"
        }
      },
      {
        "origin_id": "leadership-fundamentals",
        "course": {
          "type": "EXTERNAL",
          "title": "Leadership Fundamentals",
          "description": "Core skills for new and aspiring managers.",
          "course_url": "https://your-platform.com/courses/leadership-fundamentals"
        }
      }
    ]
  }'
```

Response:

```json theme={null}
{
  "status": "success",
  "data": {
    "task_id": "7FPJba3qRNBYaJgTvDKZi7mi"
  }
}
```

## Polling for the result

Use the `task_id` from the previous step to poll the status endpoint.

```bash theme={null}
curl --request GET \
  --url 'https://api.kombo.dev/v1/lms/courses/bulk/7FPJba3qRNBYaJgTvDKZi7mi' \
  --header 'Authorization: Bearer <api_key>' \
  --header 'X-Integration-Id: <integration_id>'
```

### While the task is pending

```json theme={null}
{
  "status": "success",
  "data": {
    "task_id": "7FPJba3qRNBYaJgTvDKZi7mi",
    "status": "PENDING",
    "created_at": "2025-03-12T14:30:00.000Z",
    "completed_at": null
  }
}
```

### When the task completes

The `data` field contains the results. The exact shape depends on the
endpoint – check the API reference for details.

```json theme={null}
{
  "status": "success",
  "data": {
    "task_id": "7FPJba3qRNBYaJgTvDKZi7mi",
    "status": "COMPLETED",
    "created_at": "2025-03-12T14:30:00.000Z",
    "completed_at": "2025-03-12T14:30:47.000Z",
    "data": [
      {
        "origin_id": "cybersecurity-101",
        "status": "SUCCEEDED",
        "data": {
          "course_id": "26vafvWSRmbhNcxJYqjCzuJg"
        }
      },
      {
        "origin_id": "data-privacy-basics",
        "status": "SUCCEEDED",
        "data": {
          "course_id": "8kQes79O9lH0FaLxktO0yza"
        }
      },
      {
        "origin_id": "leadership-fundamentals",
        "status": "FAILED",
        "error": {
          "code": "INTEGRATION.UNKNOWN_ERROR",
          "message": "The connected tool returned an unexpected error."
        }
      }
    ]
  }
}
```

### When the task fails

If the task fails before it can process all items, the response includes an
`error` object. See the [error handling guide](/guides/errors) for details on how to
interpret it.

```json theme={null}
{
  "status": "success",
  "data": {
    "task_id": "7FPJba3qRNBYaJgTvDKZi7mi",
    "status": "FAILED",
    "created_at": "2025-03-12T14:30:00.000Z",
    "completed_at": "2025-03-12T14:30:12.000Z",
    "error": {
      "code": "INTEGRATION.AUTHENTICATION_FAILED",
      "message": "The integration credentials are no longer valid."
    }
  }
}
```

## Idempotency

Async endpoints are **idempotent by default**. Sending the same request
body for the same integration returns the existing task instead of creating
a duplicate. This makes it safe to retry requests without risking duplicate
processing – for example, if your initial request times out or you're
unsure whether it went through.

Idempotency is derived automatically from the request body and
integration details. Manual idempotency keys (e.g., via a header) are not
currently supported. If this is something you need, please [let us
know](/support).

## Recommended polling strategy

Async endpoints are designed for long-running operations that complete
"eventually" rather than within seconds. The right polling interval depends
on the action and the volume of data you're processing – individual
endpoints may include more specific guidance in the API reference.

As a general rule:

* **Polling every few minutes** is a good starting point for most use
  cases.
* For very large payloads or operations that you expect to take longer,
  **polling every 30–60 minutes** may be more appropriate.
* Stop polling once you receive a terminal status (`COMPLETED` or
  `FAILED`).
* If a task stays `PENDING` longer than expected, don't poll
  indefinitely. Set a reasonable timeout on your side and treat exceeding
  it as an error. The right threshold depends on the action – check the
  API reference for endpoint-specific guidance or reach out to our
  [support](/support).
