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

# Import a full configuration snapshot, including knowledge articles

PUT https://example.ada.support/api/v2/config/
Content-Type: application/json

Applies a snapshot produced by `GET /v2/config/` — the whole configuration
document *and* the Agent's knowledge articles — reading it from an archive
you upload rather than from a request body. Use it to restore an Agent, or
to clone one Agent's configuration onto another.

## Two calls

1. `PUT /v2/config/` returns a `job_id` and an `upload` block holding a
   short-lived `url`.
2. `PUT` the raw `.tar.gz` to that `url` in one request — no form encoding,
   no `Content-Type` needed.

Then poll `job_id` at `GET /v2/config/jobs/{job_id}`. The job waits in
`waiting_for_upload` until the archive appears and starts on its own.

The bytes never pass through this API, so the archive may be far larger
than a request body allows, and a snapshot from an Agent on one region
imports onto an Agent on another with no access between them.

## `mode` defaults to `validate`

**An omitted `mode` is a dry run. Send `mode: "apply"` to write anything.**

`validate` reads the archive and reports what an apply would do, without
writing — including `changes["knowledge.articles"]["delete"]`, the number of
articles the full replace would remove. It is the only place the destructive
half is enumerated beforehand, so run it before every apply.

## Narrowing what is applied

`include` lists the sections to import; omit it for the whole snapshot.

Anything a listed section references travels with it, whichever section you
name. Ask for `playbooks`, for example, and the articles, variables, actions
and handoffs those playbooks use come too; ask for `handoffs` and the
variables their messages interpolate come with them. That leaves every
section in one of three states.

| Section                       | What happens                                                             |
| ----------------------------- | ------------------------------------------------------------------------ |
| **Listed** in `include`       | Replaced. Entities of that type the snapshot does not carry are deleted. |
| **Pulled in** as a dependency | Added to, never pruned.                                                  |
| **Neither**                   | Untouched — not read, not written.                                       |

Staying with `playbooks`: the Agent's playbooks are replaced, so any the
snapshot does not carry are deleted. Everything they dragged in is only
added to, because your selection reached some of those entities, not all.

So a playbook that uses 4 variables brings those 4 over, and the target
Agent's existing variables are not affected — no full replace in this case.

The job's `result.selection` reports what came over and what could not be
resolved.

`allow_unsafe_import` turns that dependency pull off and downgrades
pre-flight blockers to warnings. It leaves an Agent with dangling references
that nothing repairs later — never use it on one serving traffic. Validate
with the same flag first: `result.selection.unresolved` lists every
reference it will break.

## Secrets

Secret values never travel in the archive. Supply them in `secrets`, keyed
by variable name; read the key list from the source export's
`result.secrets` (or the identical block in the archive's `config.json`).

They are provisioned when the import is **requested** — before the upload,
and never persisted on the job — so a request whose upload never lands still
leaves the values provisioned.

A secret the snapshot needs that is neither supplied nor already on the
target is a **warning, not a failure**. The import completes and whatever
reads that secret fails at runtime until someone sets it.

## Uploading

The URL is valid for one hour, measured when the upload *starts*, so a slow
link finishing later is fine.

**Check the status your upload returns.** Ada never sees that request, so a
rejected upload is indistinguishable from one that never ran — the job waits,
then fails. `curl` exits `0` on an HTTP error unless you pass `-f`. To retry,
just `PUT /v2/config/` again: a job still waiting for its archive is
replaced, not rejected.

An archive that never arrives fails the job about 90 minutes after submission
(`upload_not_received`); one over the size ceiling fails as soon as it lands
(`upload_too_large`). The archive is read only when the job runs — the manifest
describing it sits at the end of the file — so a malformed snapshot fails the
**job** seconds after it starts, before anything is written, rather than
returning a `400` here.

## What an apply does

Each entity applies with the semantics the export documents: full replace
matched on the natural key.

Knowledge is **full replace**: an article the snapshot does not carry is
deleted, however many that is. Articles created after the import started are
never deleted, and articles with no knowledge source are left alone, since an
import cannot recreate one. Leaving `knowledge` out of `include` stops every
article deletion; articles that your listed sections reference are still
added or updated.

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

## Authentication

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

## Request

### Body (application/json)

This endpoint expects a ConfigSnapshotImportRequest.

- `mode` (enum, optional, default: validate) — **Defaults to `validate`. Send `mode: "apply"` explicitly to write anything** — an omitted `mode` is a dry run, not an import. `validate` writes nothing: it checks that the source is a readable, supported snapshot whose configuration document would apply to this Agent, and reports the create/update/delete counts the apply would produce — including how many knowledge articles the full replace would delete. `apply` imports the snapshot. It is a full replace with hard deletes and it is not atomic, so the two mistakes do not cost the same: a caller who meant `validate` and got `apply` has destroyed an Agent's knowledge, while one who meant `apply` and got `validate` has lost a round trip. The default is set accordingly. Both are asynchronous and both need the archive uploaded: `validate` also returns a `job_id` and an `upload`, and its verdict is the job's `result`.
  - Allowed values: `validate`, `apply`
- `secrets` (map from string to string, optional) — The Agent's client secrets, by variable name. When the AI Agent calls your systems — an HTTP-request action hitting your order service, say — it authenticates with these. They are the `client_secret`-scope variables managed under **Content > Authentication** in the Agent. Supply them because secret values never travel in the export archive. The names the snapshot needs are listed in the export's own `secrets` block, each with a `null` value — fill those in and send the result here. They are provisioned when the import is requested, before the archive is uploaded, and never stored on the job, so a request whose upload never lands still leaves the values provisioned. A secret the snapshot needs that is neither supplied nor already on the target Agent is a warning, not a failure — the actions using it fail at runtime until someone supplies it.
- `allow_unsafe_import` (boolean, optional, default: false) — **Applies the snapshot with its safety gates off, and leaves the Agent needing manual repair. Never use it on an Agent serving traffic.** With `include`, the import normally follows every reference out of the sections you named and brings the targets along, so a playbook that calls a tool arrives with that tool, a handoff arrives with the variables its messages interpolate, and a coaching entry arrives with the action it points at. `allow_unsafe_import` stops all of that: only the sections in `include` are applied, and every reference leaving them — of any kind, from any section — is left dangling unless the target Agent already has what it names. It also drops the checks that would otherwise refuse such an import. An active playbook whose tool, variable or response references do not resolve is written anyway rather than rejected. Pre-flight blockers — a handoff whose integration is not connected on this Agent, a required client feature that is off — are reported as warnings instead of failing the job. What lands is a broken Agent: playbooks holding unresolved `{action:…}` text that no-ops at runtime, handoffs pointing at integrations that do not exist, coaching entries whose target was never created. Nothing repairs it later — someone fixes each reference by hand, or imports the missing sections afterwards. It exists for rebuilding a scratch or test Agent out of one slice of a snapshot, where a half-wired Agent is more useful than a refused import. Run `mode: validate` with the same flag first: every reference you are about to break is listed in `selection.unresolved`, and every overridden blocker appears under `warnings`.
- `include` (list of enum, optional) — Import only these sections of the snapshot. Omit to import all of it. Anything a listed section references travels with it. Importing `playbooks` also brings the articles, variables, API tools and handoffs those playbooks use; importing `handoffs` brings the variables their messages interpolate. The archive itself is not filtered — the selection is applied when it is read. A section you list is replaced: entities of that type absent from the snapshot are deleted, exactly as a whole-snapshot import would. A section pulled in only as a dependency is added to, never pruned — importing one playbook cannot delete a variable it does not use. Sections you neither list nor reach are untouched. The job's `result.selection` reports what came over, including any reference that could not be resolved. Use `mode: validate` to see it before applying. `allow_unsafe_import: true` turns the dependency pull off, so only the sections listed here are applied and their outbound references dangle.
  - Allowed values: `persona`, `variables`, `actions`, `handoffs`, `processes`, `greetings`, `languages`, `playbooks`, `coaching`, `custom_instructions`, `glossary`, `knowledge`, `csat_settings`, `custom_redactions`, `image_redactions`, `integrations`, `web_chat`

## Response

### 202

Accepted — for `mode: validate` as well, which reports its verdict as the job's result rather than inline. Send the archive to `upload.url`, then poll the returned `job_id` at `GET /v2/config/jobs/{job_id}` for progress, warnings, and the result.

- `job_id` (string, required) — The id to poll for the job status and result
- `status` (enum, required) — Always `waiting_for_upload`: the job exists but cannot start until the archive arrives at `upload.url`.
  - Allowed values: `waiting_for_upload`
- `upload` (ConfigUploadTarget, required) — Where to send the configuration snapshot for an import. `{url, method, expires_at}` — send the raw `.tar.gz` bytes to `url` with a single `PUT`. `expires_at` is checked when the upload starts, so one begun before it and still running after completes normally.

## Errors

### 400 Bad Request Error

The request body is malformed — an unrecognised field, or a `mode` or `secrets` value of the wrong shape. Whether the archive you upload is a valid snapshot is reported on the job, not here.

- `errors` (list of ErrorsErrorsItems, required) — A list of 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

### 409 Conflict Error

An import is already queued or running for this Agent. Import is full-replace and not atomic, so two at once would each delete entities the other created. Poll the running job to completion before submitting another. A job still `waiting_for_upload` does **not** conflict: it has applied nothing, so submitting again replaces it and returns a fresh upload URL.

- `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

### ConfigUploadTarget

Where to send the configuration snapshot for an import. `{url, method, expires_at}` — send the raw `.tar.gz` bytes to `url` with a single `PUT`. `expires_at` is checked when the upload starts, so one begun before it and still running after completes normally.

- `url` (string, required) — Send the archive here with a single `PUT` of the raw `.tar.gz` bytes. No form encoding, and no `Content-Type` is required.
- `method` (enum, required) — The HTTP method to send the archive with
  - Allowed values: `PUT`
- `expires_at` (string, required) — When `url` stops being accepted. Checked when the upload starts, so an upload begun before this and still running after it completes normally.

### ErrorsErrorsItems

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

## Examples

### Validate selected sections

**Request**

```json
{
  "mode": "validate",
  "include": [
    "playbooks"
  ]
}
```

**Response**

```json
{
  "job_id": "config_job_97fade03e7ac4a5f88eaa23227e68aa3",
  "status": "waiting_for_upload",
  "upload": {
    "url": "https://storage.example.com/config-jobs/…/upload.tar.gz?X-Amz-Signature=…",
    "method": "PUT",
    "expires_at": "2026-09-04T15:52:59Z"
  }
}
```

**SDK Code**

```python Validate selected sections
import requests

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

payload = {
    "mode": "validate",
    "include": ["playbooks"]
}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.put(url, json=payload, headers=headers)

print(response.json())
```

```javascript Validate selected sections
const url = 'https://example.ada.support/api/v2/config/';
const options = {
  method: 'PUT',
  headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
  body: '{"mode":"validate","include":["playbooks"]}'
};

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

```go Validate selected sections
package main

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

func main() {

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

	payload := strings.NewReader("{\n  \"mode\": \"validate\",\n  \"include\": [\n    \"playbooks\"\n  ]\n}")

	req, _ := http.NewRequest("PUT", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

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

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

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

}
```

```ruby Validate selected sections
require 'uri'
require 'net/http'

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

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

request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"mode\": \"validate\",\n  \"include\": [\n    \"playbooks\"\n  ]\n}"

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

```java Validate selected sections
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.put("https://example.ada.support/api/v2/config/")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"mode\": \"validate\",\n  \"include\": [\n    \"playbooks\"\n  ]\n}")
  .asString();
```

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

$client = new \GuzzleHttp\Client();

$response = $client->request('PUT', 'https://example.ada.support/api/v2/config/', [
  'body' => '{
  "mode": "validate",
  "include": [
    "playbooks"
  ]
}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

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

```csharp Validate selected sections
using RestSharp;

var client = new RestClient("https://example.ada.support/api/v2/config/");
var request = new RestRequest(Method.PUT);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"mode\": \"validate\",\n  \"include\": [\n    \"playbooks\"\n  ]\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift Validate selected sections
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [
  "mode": "validate",
  "include": ["playbooks"]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

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

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()
```

### Apply selected sections with secrets

**Request**

```json
{
  "mode": "apply",
  "secrets": {
    "crm_api_token": "the-value"
  },
  "include": [
    "playbooks"
  ]
}
```

**Response**

```json
{
  "job_id": "config_job_3356618585504ce9a41eed22502d19e0",
  "status": "waiting_for_upload",
  "upload": {
    "url": "https://storage.example.com/config-jobs/…/upload.tar.gz?X-Amz-Signature=…",
    "method": "PUT",
    "expires_at": "2026-09-04T15:54:12Z"
  }
}
```

**SDK Code**

```python Apply selected sections with secrets
import requests

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

payload = {
    "mode": "apply",
    "secrets": { "crm_api_token": "the-value" },
    "include": ["playbooks"]
}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.put(url, json=payload, headers=headers)

print(response.json())
```

```javascript Apply selected sections with secrets
const url = 'https://example.ada.support/api/v2/config/';
const options = {
  method: 'PUT',
  headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
  body: '{"mode":"apply","secrets":{"crm_api_token":"the-value"},"include":["playbooks"]}'
};

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

```go Apply selected sections with secrets
package main

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

func main() {

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

	payload := strings.NewReader("{\n  \"mode\": \"apply\",\n  \"secrets\": {\n    \"crm_api_token\": \"the-value\"\n  },\n  \"include\": [\n    \"playbooks\"\n  ]\n}")

	req, _ := http.NewRequest("PUT", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

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

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

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

}
```

```ruby Apply selected sections with secrets
require 'uri'
require 'net/http'

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

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

request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"mode\": \"apply\",\n  \"secrets\": {\n    \"crm_api_token\": \"the-value\"\n  },\n  \"include\": [\n    \"playbooks\"\n  ]\n}"

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

```java Apply selected sections with secrets
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.put("https://example.ada.support/api/v2/config/")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"mode\": \"apply\",\n  \"secrets\": {\n    \"crm_api_token\": \"the-value\"\n  },\n  \"include\": [\n    \"playbooks\"\n  ]\n}")
  .asString();
```

```php Apply selected sections with secrets
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('PUT', 'https://example.ada.support/api/v2/config/', [
  'body' => '{
  "mode": "apply",
  "secrets": {
    "crm_api_token": "the-value"
  },
  "include": [
    "playbooks"
  ]
}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

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

```csharp Apply selected sections with secrets
using RestSharp;

var client = new RestClient("https://example.ada.support/api/v2/config/");
var request = new RestRequest(Method.PUT);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"mode\": \"apply\",\n  \"secrets\": {\n    \"crm_api_token\": \"the-value\"\n  },\n  \"include\": [\n    \"playbooks\"\n  ]\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift Apply selected sections with secrets
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [
  "mode": "apply",
  "secrets": ["crm_api_token": "the-value"],
  "include": ["playbooks"]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

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

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()
```