> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.ada.cx/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.ada.cx/_mcp/server.

# Get the status and result of a config export/import job

GET https://example.ada.support/api/v2/config/jobs/{job_id}

Poll a job submitted via `GET` or `PUT /v2/config/`. Jobs are scoped to
the authenticated Agent; an unknown or unowned `job_id` returns 404.

Three kinds of job are polled here. They share this response but not a
lifecycle, a `result` shape, or a set of failures — so read the section
for the one you submitted.

| Job | Submitted by | `type` | Lifecycle |
|---|---|---|---|
| Export | `GET /v2/config/` | `export` | `queued` → `in_progress` → `completed` |
| Import, validate | `PUT /v2/config/`, `mode: validate` (the default) | `import` | `waiting_for_upload` → `queued` → `in_progress` → `completed` |
| Import, apply | `PUT /v2/config/`, `mode: apply` | `import` | `waiting_for_upload` → `queued` → `in_progress` → `completed` |

Both import modes report `type: import` and the response never restates
the mode. Tell them apart by `result`: only a validate carries
`result.status`.

Any job can also end at `failed`, from any non-terminal status. Then
`failure_reason` is the slug to branch on and `failure_detail` the
sentence to show a person — see `ConfigJobFailureReason` for all 14 and
what to do about each.

## Export

Builds the archive in the background. Nothing about the Agent changes.

`completed` gives a **temporary download link** in `result.url`, minted
fresh on every poll, so it is never stale — poll again rather than
re-exporting if one expires. `result.secrets` lists the `client_secret`
variable names the snapshot needs, each `null`; that is the list to fill
in for the import, and it needs no download.

The one failure peculiar to export is `archive_expired`: the export
itself succeeded, but the archive was removed from storage before you
polled. The job row underneath is still `completed`; this response
reports `failed` rather than hand you a URL that 404s.

## Import — `mode: validate`

The default, and where every import should start. **Nothing is ever
written**, whatever the verdict.

The job begins at `waiting_for_upload` and moves on by itself once the
archive lands at the `upload.url` you were given — a validate needs the
upload too, since the answer is in the archive.

It reaches `completed` even when the answer is bad; the verdict is
`result.status`, `valid` or `blocked`. `result.changes` reports what an
apply would create, update and delete, per section and with names —
including `knowledge.articles.delete`, the number of articles the full
replace would remove from this Agent. On a selective import
`result.selection` reports what the `include` closure actually dragged in
and which references it could not resolve.

Run this before every apply. It is the only place the destructive half of
an import is enumerated before it happens.

## Import — `mode: apply`

Writes the snapshot to the Agent. This is the destructive direction, and
the one you must ask for by name.

Same lifecycle as a validate: it waits at `waiting_for_upload` for the
archive, and nothing is read or written before that lands — so a job
sitting there has changed nothing and costs nothing to abandon.

`completed` gives `result.articles` (`upserted` / `deleted`),
`result.indexing`, and `result.post_apply_blockers` — **empty is the
healthy answer** there; a non-empty list means the import committed but
left something broken. `warnings` lists what the import did not do,
without failing.

Two failures need different handling from the rest. `preflight_failed`
means the target Agent cannot serve the snapshot: every blocker is in
`result`, and **nothing was written**. `partially_applied` means the
opposite — the run died after the configuration phase committed, so the
Agent holds part of the snapshot and a blind retry is unsafe.

## Polling

Poll every 5 seconds for the first minute, then every 15. An import can
wait minutes for its upload to be noticed and run for hours on a large
knowledge base; `progress` moves while it does, and a job that keeps reporting is
never timed out for being slow.


Reference: https://docs.ada.cx/reference/snapshot-api/get-job

## Authentication

- `Authorization` header (bearer token, required) — Bearer authentication of the form `Bearer <token>`, where token is your auth token.

## Request

### Path parameters

- `job_id` (string, required) — The job id returned by the export/import submit endpoint

## Response

### 200

The job status, with the result once completed

- `job_id` (string, required)
- `type` (enum, required) — Which direction the job runs
  - Allowed values: `export`, `import`
- `status` (enum, required) — Current lifecycle status of the job. `waiting_for_upload` is import-only: the job exists and the archive has not arrived yet, so no work has begun and `started_at` stays null throughout.
  - Allowed values: `waiting_for_upload`, `queued`, `in_progress`, `completed`, `failed`
- `created_at` (string, optional, nullable)
- `started_at` (string, optional, nullable) — When the job started running; null while the job is still queued or waiting for its upload. The gap from `created_at` is time spent waiting — for the archive, then to start — not working.
- `completed_at` (string, optional, nullable) — When the job reached a terminal status; null while it runs
- `progress` (integer, optional, nullable) — How far through the knowledge the job has got, as a percentage. `null` until the size of the work is known, which for both directions is once the job has started and read the manifest — so a `queued` or `waiting_for_upload` job always reports `null`. Knowledge is the only phase whose duration scales with the Agent: an export counts the articles written to the archive, an import the archive's article files applied. **It answers "how far", never "is it done" — read `status` for that.** `100` does not mean finished, and appears on three different statuses: - `completed` — reported outright, since an Agent with no articles finishes with nothing counted. - `in_progress` — the phases either side of the knowledge are not counted, so an export sits at `100` while it packs and uploads the archive, and an import while it applies the configuration document. - `failed` — an export whose scan completed but whose archive expired before you polled (`failure_reason: archive_expired`). A job that fails partway keeps serving the ratio it reached, so an import that died at 4,000 of 10,000 files reports `40` from then on. One that fails before the manifest reports `null`. Floored while the job runs, so a job one article short of done reports `99`. It can go **down**: an interrupted job is restarted from the start of the knowledge, and an import resumes where it stopped rather than from what the dead attempt had merely reported.
- `result` (ConfigJobStatusResult, optional, nullable) — Null while the job is queued or in progress. Which shape it takes is settled by `type`, the submitted `mode`, and `status`: | `type` | `mode` | `status` | `result` | |---|---|---|---| | `export` | — | `completed` | `ConfigExportResponse` — a temporary link to the archive plus the client-secret names the snapshot needs. The URL is minted fresh on every poll, so it is never stale. | | `import` | `validate` | `completed` | `ConfigSnapshotValidation` — the verdict on the snapshot. Nothing was written, whether the verdict is `valid` or `blocked`. | | `import` | `apply` | `completed` | `ConfigSnapshotImportResult` — what was written, counted as it ran. Import deliberately does not re-export: the document would be prohibitively large for the Agent sizes this exists to serve. | | `import` | `apply` | `failed` | `ConfigPreflightFailure`, but only for `failure_reason: preflight_failed`. Every other failure leaves `result` null and puts its sentence in `failure_detail`. |
- `warnings` (list of ConfigImportWarning, optional, nullable) — What the import did not do, without failing — see `ConfigImportWarning` for the five kinds. An empty array once an import completes cleanly; null for an export, and null while a job runs or after it fails.
- `failure_reason` (enum, optional, nullable) — The kind of failure, as a stable slug — null unless `status` is `failed`. Branch on this rather than on `failure_detail`, whose wording is not part of the contract.
  - Allowed values: `archive_expired`, `upload_not_received`, `upload_too_large`, `source_unreadable`, `invalid_snapshot`, `preflight_failed`, `superseded`, `partially_applied`, `incomplete_knowledge`, `ref_key_already_mapped`, `timeout`, `stalled`, `internal_error`, `agent_not_found`
- `failure_detail` (string, optional, nullable) — One sentence naming what actually went wrong, to show a person. Null when the job has not failed, and null on the failures whose slug says everything there is to say — `preflight_failed` (whose findings are in `result`), `timeout`, `stalled`, `agent_not_found`, and `internal_error`, whose underlying message is never returned because driver and storage errors carry internal hostnames.

## Errors

### 401 Unauthorized Error

Unauthorized

- `errors` (list of ErrorsErrorsItems, required) — A list of errors

### 403 Forbidden Error

Forbidden

- `errors` (list of ErrorsErrorsItems, required) — A list of errors

### 404 Not Found Error

Not Found

- `errors` (list of ErrorsErrorsItems, required) — A list of errors

### 429 Too Many Requests Error

Too Many Requests

- `errors` (list of ErrorsErrorsItems, required) — A list of errors

### 500 Internal Server Error

Internal Server Error

- `errors` (list of ErrorsErrorsItems, required) — A list of errors

## Types

### ConfigJobStatusResult

Null while the job is queued or in progress. Which shape it takes is settled by `type`, the submitted `mode`, and `status`: | `type` | `mode` | `status` | `result` | |---|---|---|---| | `export` | — | `completed` | `ConfigExportResponse` — a temporary link to the archive plus the client-secret names the snapshot needs. The URL is minted fresh on every poll, so it is never stale. | | `import` | `validate` | `completed` | `ConfigSnapshotValidation` — the verdict on the snapshot. Nothing was written, whether the verdict is `valid` or `blocked`. | | `import` | `apply` | `completed` | `ConfigSnapshotImportResult` — what was written, counted as it ran. Import deliberately does not re-export: the document would be prohibitively large for the Agent sizes this exists to serve. | | `import` | `apply` | `failed` | `ConfigPreflightFailure`, but only for `failure_reason: preflight_failed`. Every other failure leaves `result` null and puts its sentence in `failure_detail`. |

### ConfigImportWarning

Something the import did not do, or did differently, without failing. Each entry is `{entity, message}` — `entity` names the config section the warning is about, `message` says what was not done and what to do about it. There is no code to branch on; these are for a person to read. Every warning is one of five kinds: | Kind | What it means | Typical `entity` | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Section ignored | The snapshot carries a section this API does not manage, or no longer manages. It was skipped whole. | the section's own name | | Reference unresolved | An entity was written, but a reference inside it names something this Agent does not have. The reference is left in place, unresolved. | `actions`, `handoffs`, `processes`, `greetings`, `playbooks`, `glossary`, `custom_instructions`, `coaching` | | Entry skipped | One entry could not be written and the rest of the section was. | `knowledge` (an article with no content or no source), `coaching` (no redacted transcript) | | Applied with less | The write happened but something around it did not. | `integrations` (the enabled set could not be merged or read back), `languages` (a read-only field), `secrets` (a required value was not supplied) | | Source mismatch | The snapshot came from a different Agent than the one importing it. Supported — this is how a clone works — but worth seeing. | `bot_handle` | Warnings never fail a job. A completed import with warnings applied everything else it was asked to.

- `entity` (string, required) — Which config section the warning is about. An open set: for an ignored section it is the section's own name, which may be one this API does not otherwise document.
- `message` (string, required) — What was not done, and what to do about it

### ErrorsErrorsItems

- `type` (string, required) — The error type
- `message` (string, required) — The error message
- `details` (string, optional, nullable) — Extra information about the error

### ConfigExportResponse

A completed config export, as a temporary link to a single downloadable archive named `config_<YYYYMMDD-HHMMSS>.tar.gz` after the moment it was taken. Unpack it to get `manifest.json`, `config.json`, and an `articles/` directory holding the `articles_*.jsonl` files. Because the export uploads one object, a URL means the export is complete — there is no partial state to guard against. `{url, expires_at, secrets}` — `url` is minted fresh on every poll, so it is never stale; `secrets` names the `client_secret` variables the snapshot needs, each `null`, which is the list to fill in for the import.

- `url` (string, required) — Temporary download link, valid until `expires_at`. A new one is minted on every poll, so poll the job again rather than re-exporting if it expires.
- `expires_at` (string, required) — When `url` stops working
- `secrets` (map from string to any, optional) — The Agent's client-secret variable names at the moment of export, each `null`. These are the credentials the AI Agent uses when its actions call your systems, managed under **Content > Authentication**. Values never leave the source Agent, so this is the list the target will need: fill each one in and send the result as the import request's `secrets`. The same block is inside the archive's `config.json`; it is repeated here so reading the list does not require downloading and unpacking the archive. Absent on an export that finished before this field existed.

### ConfigSnapshotValidation

The result of a `mode: validate` snapshot import — nothing was written. `{status, blockers, warnings, changes, source, source_bot_handle, articles, selection}` — `status` is the verdict (`valid` or `blocked`) and is the only field no other `result` shape carries, so it is how you tell a validate result from the rest. `changes` is what an apply would do; `articles` is `{found}`, the count the snapshot carries.

- `status` (enum, required) — `blocked` means the snapshot is well-formed but would break this Agent; the reasons are in `blockers`. Nothing was written either way.
  - Allowed values: `valid`, `blocked`
- `source` (string, required) — Where the validated snapshot was read from
- `articles` (ConfigSnapshotValidationArticles, required) — What the snapshot holds, read from its manifest
- `blockers` (list of ConfigPreflightFinding, optional) — Problems that would cause `mode: apply` to refuse this snapshot. Always empty under `allow_unsafe_import: true`, which reports them among the warnings instead.
- `warnings` (list of ConfigPreflightFinding, optional) — Problems that would degrade the Agent but not refuse the import, plus every blocker overridden by `allow_unsafe_import`
- `changes` (ConfigPreflightChanges, optional)
- `source_bot_handle` (string, optional, nullable) — The handle of the Agent the snapshot was exported from
- `selection` (ConfigImportSelection, optional) — What an `include` selection resolved to. `null` when the request carried no `include` and the whole snapshot was imported. `{requested, included, pulled, unresolved}` — `requested` is what you asked for, `included` everything that was applied once references were followed, `pulled` a per-section count of the extras that came with them, and `unresolved` the references the selection could not follow, each `{kind, reference, parameter, reason}`.

### ConfigSnapshotImportResult

What a completed snapshot import did, counted as it ran. `{source, source_bot_handle, articles, post_apply_blockers, resumed_from_file, indexing, selection}` — `articles` is `{upserted, deleted}`, and `post_apply_blockers` being empty is the healthy answer.

- `source` (string, required) — Where the snapshot was read from
- `articles` (ConfigSnapshotImportResultArticles, required) — What the import did to the Agent's knowledge
- `indexing` (enum, required) — Whether a search reindex was enqueued for the imported knowledge. Results are stale until it drains.
  - Allowed values: `backfill_enqueued`, `indexing_disabled`
- `source_bot_handle` (string, optional, nullable) — The handle of the Agent the snapshot was exported from
- `post_apply_blockers` (list of ConfigPreflightFinding, optional) — Problems observed on the Agent AFTER the import committed. Empty on a healthy import. A non-empty list means the import succeeded but left something broken — fix the reported problems and import a known-good snapshot, or import the export you took of the target before this import.
- `resumed_from_file` (integer, optional) — The article file this attempt started from. `0` for a run that did the whole knowledge base. Non-zero means a previous attempt was interrupted and this one resumed, so `articles.upserted` counts only what it rewrote rather than all of the knowledge.
- `selection` (ConfigImportSelection, optional) — What an `include` selection resolved to. `null` when the request carried no `include` and the whole snapshot was imported. `{requested, included, pulled, unresolved}` — `requested` is what you asked for, `included` everything that was applied once references were followed, `pulled` a per-section count of the extras that came with them, and `unresolved` the references the selection could not follow, each `{kind, reference, parameter, reason}`.

### ConfigPreflightFailure

Why a `mode: apply` import was refused before anything was written. Carried on the failed job alongside `failure_reason: preflight_failed`. `{blockers, warnings, changes}` — every entry in `blockers` and `warnings` is a `ConfigPreflightFinding`. Fix each blocker on the target Agent; re-running the same import changes nothing.

- `blockers` (list of ConfigPreflightFinding, required)
- `warnings` (list of ConfigPreflightFinding, optional)
- `changes` (ConfigPreflightChanges, optional)

### ConfigSnapshotValidationArticles

What the snapshot holds, read from its manifest

- `found` (integer, optional) — How many articles the snapshot carries

### ConfigPreflightFinding

One reason an import would break the Agent (a blocker) or degrade it (a warning). Blockers refuse the import; warnings do not. `{code, entity, parameter, message, reference}` — branch on `code`, show `message`, and use `parameter` (a JSONPath into the document) to point at the offending value. `reference` is the thing that could not be resolved, or `null`.

- `code` (string, required) — Stable identifier for the kind of problem. Branch on this rather than on the message text.
- `entity` (string, required) — Which config section the problem is in
- `parameter` (string, required) — JSONPath to the offending value in the submitted document
- `message` (string, required) — What is wrong and what to do about it
- `reference` (string, optional, nullable) — The thing that could not be resolved, when there is one

### ConfigPreflightChanges

- `create` (integer, optional)
- `update` (integer, optional)
- `delete` (integer, optional)
- `created` (list of string, optional) — Names of the entities this import would create, up to 50. Always empty for a settings section, which has no entities to create.
- `updated` (list of string, optional) — Names of the entities this import would update, up to 50 — or, for a settings section, the dotted paths of the fields it would change
- `deleted` (list of string, optional) — Names of the entities this import would delete, up to 50. Read from the target Agent, since the payload that removes them does not carry them.
- `created_truncated` (boolean, optional) — Present and `true` when `create` exceeds the names listed in `created`
- `updated_truncated` (boolean, optional) — Present and `true` when `update` exceeds the names listed in `updated`
- `deleted_truncated` (boolean, optional) — Present and `true` when `delete` exceeds the names listed in `deleted`

### ConfigImportSelection

What an `include` selection resolved to. `null` when the request carried no `include` and the whole snapshot was imported. `{requested, included, pulled, unresolved}` — `requested` is what you asked for, `included` everything that was applied once references were followed, `pulled` a per-section count of the extras that came with them, and `unresolved` the references the selection could not follow, each `{kind, reference, parameter, reason}`.

- `requested` (list of string, optional) — The sections the request asked for, as sent
- `included` (list of string, optional) — Every section that was applied — the requested ones plus each one reached by following their references
- `pulled` (map from string to any, optional) — How many entities came over per section that was NOT requested. These sections were added to, never pruned.
- `unresolved` (list of ConfigImportSelectionUnresolvedItems, optional) — References the selection could not follow — either naming something the snapshot does not carry, or held in a field the export does not make portable. Each one is a link that will not work on the target unless the entity is already there.

### ConfigSnapshotImportResultArticles

What the import did to the Agent's knowledge

- `upserted` (integer, optional) — Articles created or updated in place
- `deleted` (integer, optional) — Articles removed because the snapshot did not carry them

### ConfigImportSelectionUnresolvedItems

- `kind` (string, optional) — What kind of entity the reference points at
- `reference` (string, optional) — The reference as it appears in the snapshot
- `parameter` (string, optional) — Where in the snapshot the reference sits
- `reason` (string, optional) — Why it could not be followed

## Examples

**Response**

```json
{
  "job_id": "config_job_97fade03e7ac4a5f88eaa23227e68aa3",
  "type": "import",
  "status": "completed",
  "created_at": "2026-09-04T14:52:59Z",
  "started_at": "2026-09-04T14:53:04Z",
  "completed_at": "2026-09-04T14:53:06Z",
  "progress": 100,
  "result": {
    "status": "valid",
    "source": "s3://…/upload.tar.gz",
    "source_bot_handle": "EXAMPLE",
    "articles": {
      "found": 0
    },
    "selection": {
      "requested": [
        "playbooks"
      ],
      "included": [
        "actions",
        "playbooks",
        "variables"
      ],
      "pulled": {
        "actions": 1,
        "variables": 1
      },
      "unresolved": []
    },
    "blockers": [],
    "warnings": [],
    "changes": {
      "variables": {
        "create": 0,
        "update": 1,
        "delete": 0,
        "created": [],
        "updated": [
          "order_id"
        ],
        "deleted": []
      },
      "actions": {
        "create": 0,
        "update": 1,
        "delete": 0,
        "created": [],
        "updated": [
          "Get order status"
        ],
        "deleted": []
      },
      "playbooks": {
        "create": 0,
        "update": 1,
        "delete": 0,
        "created": [],
        "updated": [
          "Order status"
        ],
        "deleted": []
      }
    }
  },
  "warnings": [],
  "failure_reason": null,
  "failure_detail": null
}
```

**SDK Code**

```python
import requests

url = "https://example.ada.support/api/v2/config/jobs/config_job_97fade03e7ac4a5f88eaa23227e68aa3"

headers = {"Authorization": "Bearer <token>"}

response = requests.get(url, headers=headers)

print(response.json())
```

```javascript
const url = 'https://example.ada.support/api/v2/config/jobs/config_job_97fade03e7ac4a5f88eaa23227e68aa3';
const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://example.ada.support/api/v2/config/jobs/config_job_97fade03e7ac4a5f88eaa23227e68aa3"

	req, _ := http.NewRequest("GET", url, nil)

	req.Header.Add("Authorization", "Bearer <token>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://example.ada.support/api/v2/config/jobs/config_job_97fade03e7ac4a5f88eaa23227e68aa3")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.get("https://example.ada.support/api/v2/config/jobs/config_job_97fade03e7ac4a5f88eaa23227e68aa3")
  .header("Authorization", "Bearer <token>")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://example.ada.support/api/v2/config/jobs/config_job_97fade03e7ac4a5f88eaa23227e68aa3', [
  'headers' => [
    'Authorization' => 'Bearer <token>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://example.ada.support/api/v2/config/jobs/config_job_97fade03e7ac4a5f88eaa23227e68aa3");
var request = new RestRequest(Method.GET);
request.AddHeader("Authorization", "Bearer <token>");
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = ["Authorization": "Bearer <token>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://example.ada.support/api/v2/config/jobs/config_job_97fade03e7ac4a5f88eaa23227e68aa3")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```