If you import or resync API references through the Customer API or the d360 CLI, you need to move your integration to the new asynchronous endpoints and commands before the old ones are removed. After you migrate, large specification files import without timeouts or memory errors, and your pipeline gets a clear pass or fail result for every import.
This guide walks you through the migration for Customer API V3, Customer API v2, and the d360 CLI.
On Customer API V3, force_import now defaults to false on every import and resync route, including the deprecated synchronous ones. If your V3 calls omit force_import, a specification that used to import despite errors or warnings is now refused, even before you migrate. See Decide how to handle findings.
Find out what you need to migrate
Start with the integration you use today. Each row links to the section you need.
| If you use | What to do | Go to |
|---|---|---|
Customer API V3 (/v3/projects/{projectId}/api-references) |
Replace the import and resync calls, and add polling | Migrate a Customer API V3 integration |
Customer API v2 (/v2/APIReferences) |
Replace the import and resync calls, and add polling | Migrate a Customer API v2 integration |
CI/CD routes (/v2/apidocs/…) called directly |
Replace the routes, and add polling | Migrate a Customer API v2 integration |
d360 apiref import or d360 apiref resync |
Upgrade the CLI and switch to d360 apiref submit |
Migrate the d360 CLI and CI/CD pipelines |
d360 apidocs or d360 apidocs:resync |
Upgrade the CLI and switch to d360 apidocs:submit |
Migrate the d360 CLI and CI/CD pipelines |
| The knowledge base portal only | Nothing. Portal import and resync are unaffected. |
Your current endpoints and commands still work, but they are deprecated.
The old synchronous endpoints and CLI commands are planned for removal around the end of November 2026. This date is not yet confirmed. Document360 will announce the final removal date before anything is removed. Plan your migration ahead of that window.
Before you begin
Confirm the following so your first asynchronous call works:
-
Your API key or role has both permissions. Submitting needs Update articles. Reading the outcome needs View project settings. With only the first, your submit succeeds and every poll returns
403. -
You know your regional base URL.
Region Base URL EU https://apihub.document360.ioUS https://apihub.us.document360.ioCanada https://apihub.ca.document360.ioPrivate hosting https://apihub.<your-hosting-name>.document360.io -
Your specification is within the size limits. A specification can be up to 25 MB, uploaded or fetched from a URL. The upload request can be up to 26 MB. A URL fetch that does not complete within 100 seconds fails.
-
You have decided how to handle findings. See the next section.
Decide how to handle findings
When force import is off, your import or resync is refused if the specification:
- Has any error or warning.
- Has no
serversentry.
Force import is off by default in all three integrations:
| Integration | Default | What changed |
|---|---|---|
| Customer API V3 | force_import: false |
The default used to be true. This applies to the new and the deprecated V3 routes. |
| Customer API v2 | forceImport=false |
Nothing. The default was already false. |
d360 CLI |
--force-import and --force are off unless you pass them |
Nothing. |
When a specification is refused, you have two options:
- Fix the specification. This is the recommended option. Findings can affect the generated documentation. For example, a property typed
boolinstead ofbooleanis imported without a type. - Force the import. Send
"force_import": true(V3),forceImport=true(v2),--force-import(apiref submit), or--force(apidocs:submit). Treat this as a deliberate override, not a default.
If your V3 scripts relied on the old default, they are refused now, whether or not you have migrated. Fix the specification or add force_import: true.
The same confirmation applies to a resync that would remove endpoints. Without it, the operation fails with a warning such as "This update will permanently delete 19 endpoint(s). Pass force_import=true to confirm", and lists the endpoints in result.deleted_endpoints. Nothing is deleted until you confirm.
How your integration changes
Today, your call waits until the whole specification is processed and returns the result. After you migrate, your integration works in three steps:
- Submit the import or resync. You get
202 Acceptedand an operation ID right away. - Poll the status endpoint with that ID until the operation reaches a final status.
- Read the result, errors, warnings, and the new API reference ID from the final response.
What gets imported does not change. The same specification produces the same API reference. Only when you get the result and where you read errors and warnings change.
A typical import finishes in a few seconds to under a minute. Never treat the 202 response as success.
Migrate a Customer API V3 integration
Authenticate with the X-API-Key: d360_sk_… header, or a bearer token with the customerApi scope.
Step 1: Replace the endpoints
| Operation | Old (deprecated) | New |
|---|---|---|
| Import | POST /v3/projects/{projectId}/api-references |
POST /v3/projects/{projectId}/api-references/imports |
| Resync | PUT /v3/projects/{projectId}/api-references/{apiReferenceId} |
POST /v3/projects/{projectId}/api-references/{apiReferenceId}/resyncs |
| Read the outcome | In the response | GET /v3/projects/{projectId}/operations/{operationId} |
Step 2: Submit the import or resync
To import from a URL:
curl -i -X POST "https://apihub.document360.io/v3/projects/$PROJECT_ID/api-references/imports" \
-H "X-API-Key: $D360_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.example.com/openapi.json",
"workspace_id": "<workspace-id>"
}'
To import from a file, send multipart/form-data:
curl -i -X POST "https://apihub.document360.io/v3/projects/$PROJECT_ID/api-references/imports" \
-H "X-API-Key: $D360_API_KEY" \
-F "file=@openapi.json;type=application/json" \
-F "workspace_id=<workspace-id>"
To resync an existing API reference:
curl -i -X POST "https://apihub.document360.io/v3/projects/$PROJECT_ID/api-references/$API_REFERENCE_ID/resyncs" \
-H "X-API-Key: $D360_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://api.example.com/openapi.json" }'
A resync does not take workspace_id. It uses the workspace of the existing API reference.
Use these request fields:
| Field | Import | Resync | Notes |
|---|---|---|---|
url |
Optional | Optional | A publicly reachable specification URL |
file |
Optional (multipart) | Optional (multipart) | The specification file, JSON or YAML |
content |
Deprecated | Deprecated | Use file instead |
workspace_id |
Required | Not used | Target workspace |
publish_articles |
Optional | Optional | Publishes the generated articles |
user_id |
Required with publish_articles |
Required with publish_articles |
The team member who publishes |
force_import |
Optional, default false |
Optional, default false |
See Decide how to handle findings |
Supply exactly one of url, file, or content.
The response is 202 Accepted. Save the operation_id field or the Operation-Location header. Both identify the operation you poll next.
{
"data": {
"id": null,
"status": null,
"operation_id": "4da2313e-b2da-440f-b4be-a1632901f2eb",
"operation_status": "queued"
},
"success": true
}
If your integration reads data.id or data.status from the submit response, update it. Both are null on a 202 response because nothing has been imported yet. You get the API reference ID from the operation result in Step 4.
Step 3: Poll until the operation finishes
curl -sS "https://apihub.document360.io/v3/projects/$PROJECT_ID/operations/$OPERATION_ID" \
-H "X-API-Key: $D360_API_KEY"
While the operation is queued or running, the response includes a Retry-After: 5 header. Wait that many seconds between polls. Stop polling when the status is final:
| Status | Final | Meaning |
|---|---|---|
queued |
No | Accepted and waiting for a worker |
running |
No | Being processed |
succeeded |
Yes | Imported or resynced successfully |
failed |
Yes | Not applied. Check message, errors, and result |
Cap your polling with a timeout. Twenty minutes is generous for most specifications. If an operation stops making progress, a background watchdog marks it failed, so your poll always reaches a final status.
Step 4: Read the result
When the operation succeeds, save result.api_definition_id. This is the API reference ID you use in later resync, publish, and delete calls.
{
"data": {
"id": "4da2313e-b2da-440f-b4be-a1632901f2eb",
"operation_type": "import-api-reference",
"status": "succeeded",
"message": null,
"result": {
"api_definition_id": "f9af5181-ee76-489d-b6c9-306e9eb6d60a",
"is_success": true,
"articles_created": 19,
"categories_created": 3,
"errors": null,
"warnings": null
},
"errors": null,
"completed_at": "2026-10-05T10:02:30.866Z"
},
"success": true
}
If your specification has findings and force_import is false, the operation ends as failed and lists the findings in result.errors and result.warnings. Each pointer is a JSON pointer into your specification, so you can go straight to the line to fix.
{
"data": {
"status": "failed",
"message": "The operation failed.",
"result": {
"is_success": false,
"errors": [
{
"message": "Invalid schema type identifier: bool",
"pointer": "#/paths/~1orders/get/responses/200/content/application~1json/schema/properties/active/type"
}
],
"warnings": []
}
}
}
If the job could not run, for example because the URL could not be fetched, message and errors[0].message give the reason:
{
"data": {
"status": "failed",
"message": "This operation can't be completed. Please ensure that you have given a valid specification URL.",
"result": null,
"errors": [
{ "code": "OPERATION_FAILED", "message": "This operation can't be completed. Please ensure that you have given a valid specification URL." }
]
}
}
Example: Gate a pipeline step on the import result
This script submits an import, waits for a final status, and exits with a non-zero code on failure:
#!/usr/bin/env bash
set -euo pipefail
BASE="https://apihub.document360.io"
H=(-H "X-API-Key: $D360_API_KEY")
# 1. Submit and capture the status URL from the Operation-Location header
OP_URL=$(curl -sS -D - -o /dev/null -X POST \
"$BASE/v3/projects/$PROJECT_ID/api-references/imports" "${H[@]}" \
-H "Content-Type: application/json" \
-d "{\"url\":\"$SPEC_URL\",\"workspace_id\":\"$WORKSPACE_ID\"}" \
| awk 'tolower($1)=="operation-location:" {print $2}' | tr -d '\r')
[ -n "$OP_URL" ] || { echo "Submission was not accepted" >&2; exit 1; }
# 2. Poll until a final status (capped at 20 minutes)
for _ in $(seq 1 240); do
BODY=$(curl -sS "$OP_URL" "${H[@]}")
STATUS=$(echo "$BODY" | jq -r '.data.status')
case "$STATUS" in
queued|running) sleep 5 ;;
succeeded) echo "$BODY" | jq '.data.result'; exit 0 ;;
*) echo "$BODY" | jq '.data.message, .data.result.errors, .data.result.warnings' >&2; exit 1 ;;
esac
done
echo "Timed out waiting for the operation" >&2
exit 1
Migrate a Customer API v2 integration
Authenticate with the api_token header. The v2 asynchronous endpoints take the same multipart/form-data fields as the synchronous ones, including the required forceImport.
Step 1: Replace the endpoints
| Operation | Old (deprecated) | New |
|---|---|---|
| Import | POST /v2/APIReferences |
POST /v2/APIReferences/jobs |
| Resync | PUT /v2/APIReferences |
PUT /v2/APIReferences/jobs |
| Read the outcome | In the response | GET /v2/APIReferences/jobs/{backgroundTaskId} |
If you call the CI/CD routes directly with a CI/CD API key, replace these instead. The d360 apidocs commands use the same routes.
| Operation | Old (deprecated) | New |
|---|---|---|
| Import | POST /v2/apidocs/import/{userId} |
POST /v2/apidocs/jobs/import/{userId} |
| Resync | POST /v2/apidocs/{apiDefinitionId}/resync/{userId} |
POST /v2/apidocs/jobs/{apiDefinitionId}/resync/{userId} |
| Read the outcome | In the response | GET /v2/apidocs/jobs/{backgroundTaskId} |
Step 2: Submit the import or resync
To import:
curl -i -X POST "https://apihub.document360.io/v2/APIReferences/jobs" \
-H "api_token: $D360_API_KEY" \
-F "url=https://api.example.com/openapi.json" \
-F "projectVersionId=<workspace-id>" \
-F "userId=<user-id>" \
-F "forceImport=false"
To resync:
curl -i -X PUT "https://apihub.document360.io/v2/APIReferences/jobs" \
-H "api_token: $D360_API_KEY" \
-F "url=https://api.example.com/openapi.json" \
-F "apiReferenceId=<api-reference-id>" \
-F "projectVersionId=<workspace-id>" \
-F "userId=<user-id>" \
-F "forceImport=false"
The response is 202 Accepted. Save background_task_id for polling.
{
"data": {
"background_task_id": "c02535d8-f051-41d3-92a9-57a224022f7d",
"is_processing": true,
"alerts": [{ "message": "Accepted for processing." }]
},
"success": true
}
Step 3: Poll until the job finishes
curl -sS "https://apihub.document360.io/v2/APIReferences/jobs/$BACKGROUND_TASK_ID" \
-H "api_token: $D360_API_KEY"
Keep polling until data.is_processing is false.
Step 4: Read the result
The final response carries the result. Save api_reference_id for later calls.
{
"data": {
"api_reference_id": "b8e74ec5-7de3-4d4b-8cbc-b2945f7aeb56",
"articles_created": 19,
"categories_created": 3,
"errors": [],
"alerts": [],
"background_task_id": "c02535d8-f051-41d3-92a9-57a224022f7d",
"is_processing": false
},
"success": true
}
Read findings from data.errors, and warnings from data.alerts. For a resync, data.deleted_endpoints lists the endpoints that were removed, with the method, path, article_id, and title of each.
Migrate the d360 CLI and CI/CD pipelines
The CLI has two command families: V3 (apiref) and v2 (apidocs). Stay within your current family when you migrate. Replace V3 commands with V3 commands, and v2 commands with v2 commands.
Step 1: Upgrade the CLI
The new commands require version 3.0.7 or later.
npm install -g d360@latest
d360 --version
The old commands (apiref import, apiref resync, apidocs, apidocs:resync) are marked [DEPRECATED] in --help. They stop working when the endpoints they call are removed. d360 apidocs:validate is not deprecated.
Step 2: Check that you use the right credential
Both families read the same D360_API_KEY variable name, but each expects a different credential. Using the wrong one is the most common setup error.
V3 commands (apiref, operations) |
v2 commands (apidocs, apidocs:submit, apidocs:status) |
|
|---|---|---|
| Credential | A V3 API key starting with d360_sk_, created under Settings > Knowledge base portal > API keys |
The project's CI/CD API key (a GUID) |
| Sent as | X-API-Key header |
api_token |
| Also needs | Project ID (--project-id or d360 config set-project-id), plus the workspace ID (import) or API reference ID (resync) |
User ID (--userId), plus the workspace ID (--versionId, import) or API reference ID (--apiReferenceId, resync). No project ID. |
| Permissions | Update articles to submit, View project settings to poll | The same key is used for submit and status |
The CLI has no interactive login. It takes the API key from the first source that is set: a command flag, an environment variable (D360_API_KEY, D360_PROJECT_ID, D360_BASE_URL), then the saved configuration. In a pipeline, store the key in your CI secret store and pass it as an environment variable. Use the base URL for your region.
Step 3: Replace your commands
If you use V3 commands
Replace both apiref import and apiref resync with apiref submit. The ID you pass decides the operation: --workspace-id imports, --api-reference-id resyncs.
Before:
d360 apiref import --workspace-id <workspace-id> --url https://api.example.com/openapi.json
d360 apiref resync --api-reference-id <api-reference-id> --url https://api.example.com/openapi.json
After:
# Import a new API reference and wait for the result
d360 apiref submit --workspace-id <workspace-id> --url https://api.example.com/openapi.json --wait
# Resync an existing API reference from a local file and wait
d360 apiref submit --api-reference-id <api-reference-id> --file ./openapi.json --wait
# Submit now and poll separately, for example from another pipeline stage
id=$(d360 apiref submit --workspace-id <workspace-id> --file ./openapi.json \
--filter operation_id -o tsv)
d360 operations wait "$id"
Update your flags:
| Old | New | |
|---|---|---|
| Import or resync | Two commands | One command: --workspace-id imports, --api-reference-id resyncs |
| Specification source | Import: --url or --content. Resync: --url |
--url or --file for both |
| Publish after import | --publish-articles |
--publish-articles with --user-id (required) |
| Warnings | --force-import to continue past warnings (off by default) |
|
| Result | In the response | --wait, or d360 operations wait <operation-id> |
The --filter flag takes a path into the response data. Use operation_id, not data.operation_id. The -o tsv option prints the bare ID.
--wait and d360 operations wait exit 0 when the operation succeeds. They exit 1 when it fails or the 20-minute wait budget runs out. A budget timeout stops waiting but does not cancel the operation.
If you use v2 (apidocs) commands
These are the commands shown for CI/CD in the portal's Add API reference and Edit API reference panels. Replace both apidocs and apidocs:resync with apidocs:submit, then read the result with apidocs:status. The flag names stay the same (camelCase), and you keep the CI/CD key.
Before:
d360 apidocs --apiKey "$D360_CICD_KEY" --userId <user-id> --versionId <workspace-id> \
--path ./openapi.json
d360 apidocs:resync --apiKey "$D360_CICD_KEY" --userId <user-id> \
--apiReferenceId <api-reference-id> --path ./openapi.json
After:
# Import (prints the task ID on its own line)
d360 apidocs:submit --apiKey "$D360_CICD_KEY" --userId <user-id> --versionId <workspace-id> \
--path ./openapi.json
# Resync (passing --apiReferenceId makes it a resync)
d360 apidocs:submit --apiKey "$D360_CICD_KEY" --userId <user-id> \
--apiReferenceId <api-reference-id> --path ./openapi.json
# Wait for the job to finish
d360 apidocs:status <task-id> --apiKey "$D360_CICD_KEY" --wait
apidocs:submit accepts the same flags as the old apidocs command: --apiKey, --userId, --versionId, --apiReferenceId, --path, --force, --publish, and --apihubUrl. It prints a task ID and exits.
apidocs:status takes only --apiKey, plus --apihubUrl for non-EU regions. Always gate your pipeline on --wait. Without it, the command reports once and exits 0 even if the job is still running. With --wait, it polls until the job finishes, prints the findings, and exits 1 if the job did not succeed.
If a resync would delete endpoints, it stops and lists them. Re-run with --force to confirm.
Step 4: Update your pipeline
Use one of these GitHub Actions steps as a starting point. In both, the step fails when the job fails, and the findings are printed in the log.
V3:
- name: Publish API reference (V3)
env:
D360_API_KEY: ${{ secrets.D360_API_KEY }} # a d360_sk_ key
D360_PROJECT_ID: ${{ vars.D360_PROJECT_ID }}
run: |
npm install -g d360@latest
d360 apiref submit --api-reference-id "${{ vars.D360_API_REFERENCE_ID }}" \
--file ./openapi.json --wait
v2 (apidocs):
- name: Publish API reference (v2)
env:
D360_CICD_KEY: ${{ secrets.D360_CICD_KEY }} # the project's CI/CD key
run: |
npm install -g d360@latest
task=$(d360 apidocs:submit --apiKey "$D360_CICD_KEY" --userId "${{ vars.D360_USER_ID }}" \
--apiReferenceId "${{ vars.D360_API_REFERENCE_ID }}" --path ./openapi.json)
d360 apidocs:status "$task" --apiKey "$D360_CICD_KEY" --wait
Fix common issues after you migrate
| What you see | Why it happens | What to do |
|---|---|---|
Polling returns 403 after a successful submit |
Your key or role can update articles but cannot view project settings | Give the integration both permissions |
Submit returns 409 Conflict |
Another import or resync is already running in the project. Only one can run at a time. | Poll the running task named in the response, then retry |
| Re-submitting returns the same operation ID | You sent an identical request while the first was still running | Poll the returned operation. No second import is started. |
| A V3 import or resync that used to work is now refused | force_import now defaults to false on all V3 routes, including the deprecated ones |
Fix the findings, or set force_import: true deliberately. See Decide how to handle findings. |
| A resync fails with "This update will permanently delete…" | The resync would remove endpoints | Review result.deleted_endpoints, then resubmit with force_import: true to confirm |
A submit times out or returns 5xx |
The request may or may not have been accepted | Do not retry right away. A retry can start a second import. Check for a running operation with GET /v3/projects/{projectId}/operations?status=running, or poll the operation ID if you have it. |
The CLI follows the same retry rule. It does not retry a submit after a 5xx or a dropped connection. It only retries a 429, which is rejected before any work starts.
Verify your migration
Your migration is complete when:
- Every import and resync call uses an asynchronous endpoint or command.
- Your integration reads the outcome from the status endpoint, not from the
202response. - On V3, your integration uses
operation_idinstead ofdata.idanddata.statusfrom the submit response. - Your integration handles
409 Conflictby waiting for the running task and retrying. - You have chosen how to handle findings: fix the specification, or set
force_import: truedeliberately. - Your API key or role can both submit and read operations.
- Your CLI is on version 3.0.7 or later, and each family uses its own credential: a
d360_sk_key forapiref, and the CI/CD key forapidocs. - Your pipeline fails the step when an import or resync fails.