> ## Documentation Index
> Fetch the complete documentation index at: https://bisibility.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloud import

> Import keywords and rank history with a migration token.

Cloud import accepts an export package and restores keywords plus optional rank
history into the project associated with a migration token. Use the single
request endpoint for small packages and the session protocol for large packages.

The versioned paths are included in `/api/v1/openapi.json`. The unversioned
paths are kept for CLI compatibility.

## Endpoints

```http theme={null}
POST /api/cloud/import
POST /api/v1/cloud/import
GET  /api/cloud/import/compatibility
GET  /api/v1/cloud/import/compatibility
POST /api/cloud/import/sessions
POST /api/v1/cloud/import/sessions
PUT  /api/cloud/import/sessions/{sessionId}/chunks/{index}
PUT  /api/v1/cloud/import/sessions/{sessionId}/chunks/{index}
POST /api/cloud/import/sessions/{sessionId}/finalize
POST /api/v1/cloud/import/sessions/{sessionId}/finalize
```

## Authentication

Cloud import uses a migration token, not a `bsk_live_...` API key. Header auth is
preferred:

```text theme={null}
Authorization: Bearer mig_...
```

The import endpoints also accept `Migration-Token: mig_...`. Every cloud
import endpoint (single-request import, sessions, chunks, and finalize) also
accepts JSON-body token fields for clients that cannot set headers: `token`,
`migrationToken`, or `migration_token`, either at the root or inside a nested
`export` or `data` object.

Invalid, expired, or already-used tokens return `419` with an `unauthorized`
problem response. Missing or malformed tokens return `401`.

## Migration Tokens

A migration token is bound to one project by design: it authorizes importing
into exactly the project it was minted for, is single use, and expires after
`BISIBILITY_MIGRATION_TOKEN_TTL_MINUTES`. This is what lets the exporting side
push data into a destination project without ever holding that project's API
key. There is no instance-global token; a broader credential would grant more
than an import needs.

Tokens are minted and managed through the ordinary project-scoped REST API
using an `Authorization: Bearer <api_key>` key for the destination project:

| Method   | Path                                                        | Purpose                                             |
| -------- | ----------------------------------------------------------- | --------------------------------------------------- |
| `GET`    | `/api/v1/projects/{project_id}/migration-tokens`            | List migration tokens without exposing raw secrets. |
| `POST`   | `/api/v1/projects/{project_id}/migration-tokens`            | Mint a `full` or `keywords` migration token.        |
| `DELETE` | `/api/v1/projects/{project_id}/migration-tokens/{token_id}` | Revoke through the project-scoped route.            |
| `DELETE` | `/api/v1/migration-tokens/{token_id}`                       | Revoke through the short route.                     |

The raw `mig_...` value is returned only when minted. Tokens can also be minted
from the in-app wizard.

## Compatibility Preflight

Call the public compatibility endpoint before generating an export:

```http theme={null}
GET /api/v1/cloud/import/compatibility
```

Response:

```json theme={null}
{
  "schema_versions_supported": [1, 2, 3],
  "app_version": "0.1.0",
  "latest_migration": "20260708203000_project_write_mode_actor"
}
```

The endpoint is anonymous and rate limited. `latest_migration` is `null` when
the migration table cannot be read.

## Single Request Import

Post the export package directly to `/api/v1/cloud/import`:

```json theme={null}
{
  "exportedAt": "2026-06-28T10:00:00.000Z",
  "projectId": "self_hosted_project",
  "scope": "history",
  "keywords": [
    {
      "keyword": "rank tracker api",
      "location": "United States",
      "device": "desktop",
      "targetUrl": "https://example.com/rank-tracker",
      "tags": ["api"],
      "rankingHistory": [{ "checkedAt": "2026-06-20T10:00:00.000Z", "position": 3 }]
    }
  ]
}
```

The endpoint also accepts nested `{ "export": { ... } }` and `{ "data": { ... } }`
payloads. Top-level sections include `keywords`, `alertRules` or `alert_rules`,
`competitors`, `notificationPreferences` or `notification_preferences`,
`savedViews` or `saved_views`, and CLI-style `rank_checks`.

```json theme={null}
{
  "token": "mig_...",
  "export": {
    "keywords": [
      { "text": "rank tracker api" }
    ]
  }
}
```

Top-level fields:

| Field                                                   | Description                                                 |
| ------------------------------------------------------- | ----------------------------------------------------------- |
| `keywords`                                              | Optional. Up to 500 keyword records. Defaults to `[]`.      |
| `exportedAt`                                            | Optional ISO-8601 timestamp.                                |
| `projectId`                                             | Optional source project id. Stored for import context only. |
| `scope`                                                 | Optional. `current` or `history`.                           |
| `alertRules` or `alert_rules`                           | Optional alert-rule records.                                |
| `competitors`                                           | Optional competitor records.                                |
| `notificationPreferences` or `notification_preferences` | Optional notification preference records.                   |
| `savedViews` or `saved_views`                           | Optional saved-view records.                                |

Keyword fields:

| Field                       | Description                                         |
| --------------------------- | --------------------------------------------------- |
| `keyword` or `text`         | Required keyword text.                              |
| `device`                    | `desktop` or `mobile`. Defaults to `desktop`.       |
| `location` or `country`     | Location label. Defaults to `United States`.        |
| `targetUrl` or `target_url` | Absolute URL, path, `null`, or omitted.             |
| `tags`                      | Up to 12 tags. Defaults to `[]`.                    |
| `rankingHistory`            | Optional. Up to 5000 historical checks per keyword. |

History fields:

| Field              | Description                             |
| ------------------ | --------------------------------------- |
| `checkedAt`        | Required ISO-8601 timestamp.            |
| `position`         | Integer position or `null`.             |
| `previousPosition` | Integer previous position or `null`.    |
| `rankingUrl`       | Absolute URL, path, `null`, or omitted. |

Imported history rows use provider value `self-hosted-import`.

<Warning>
  Historical rows may be nested under each keyword's `rankingHistory` array or
  sent in a CLI-style top-level `rank_checks` array. Top-level checks are merged
  into keyword history by source keyword id first, then by keyword text. Checks
  that match no keyword are dropped and are counted only in `history_received`.
</Warning>

Success returns `201`.

```json theme={null}
{
  "counts": {
    "keywords": 1,
    "keywords_created": 1,
    "keywords_skipped": 0,
    "history": 1,
    "history_received": 1
  },
  "job_id": "job_abc123",
  "state": "done"
}
```

Section counts are included when the corresponding section was present in the
request:

| Count                                | Meaning                            |
| ------------------------------------ | ---------------------------------- |
| `alert_rules`, `alert_rules_skipped` | Imported and skipped alert rules.  |
| `competitors`                        | Imported competitors.              |
| `notification_preferences`           | Imported notification preferences. |
| `saved_views`                        | Imported saved views.              |

## Chunked Session Import

Use sessions when the package is large enough that one request may exceed body
limits or timeout. A session has three phases: create, upload chunks, finalize.

### 1. Create the Session

```http theme={null}
POST /api/v1/cloud/import/sessions
Authorization: Bearer mig_...
Content-Type: application/json
```

```json theme={null}
{
  "version": 3,
  "chunk_count": 3,
  "totals": {
    "keywords": 650,
    "rank_checks": 12000
  }
}
```

`version` must be `3`. `chunk_count` must be between `1` and `500`. `totals` is
optional and is stored with the session manifest.

Success returns `201`.

```json theme={null}
{
  "session_id": "job_abc123",
  "state": "receiving",
  "chunk_limits": {
    "max_body_bytes": 8388608,
    "max_history_rows": 25000,
    "max_keywords": 500
  }
}
```

Only one live `receiving` or `importing` session is allowed per token. A second
live session returns `409`.

### 2. Upload Chunks

Upload each chunk by zero-based index:

```http theme={null}
PUT /api/v1/cloud/import/sessions/job_abc123/chunks/0
Authorization: Bearer mig_...
Content-Type: application/json
```

Keyword chunks carry keyword rows:

```json theme={null}
{
  "checksum": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "kind": "keywords",
  "keywords": [{ "keyword": "rank tracker api", "rankingHistory": [] }]
}
```

Section chunks carry non-keyword package sections and optional source keyword
ids used to map imported alert targets:

```json theme={null}
{
  "checksum": "sha256:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd",
  "kind": "sections",
  "sections": {
    "competitors": [{ "domain": "example.com", "label": "Example" }],
    "source_keyword_ids": {
      "kw_source_1": {
        "text": "rank tracker api",
        "location": "United States",
        "device": "desktop"
      }
    }
  }
}
```

The checksum is the chunk content identity for idempotent retries. Retrying the
same `{sessionId, index}` with the same checksum returns `200`; retrying that
index with a different checksum returns `409`. Clients should compute the
`sha256:` value over the JSON bytes they intend to send before gzip compression.

To gzip a chunk, send the gzip-compressed JSON body with:

```text theme={null}
Content-Encoding: gzip
```

The body limit applies before and after decompression. Oversized chunks return
`413`.

Success returns:

```json theme={null}
{
  "state": "receiving",
  "chunks_received": 2,
  "chunk_count": 3
}
```

### 3. Finalize

Finalize after every chunk has been accepted:

```http theme={null}
POST /api/v1/cloud/import/sessions/job_abc123/finalize
Authorization: Bearer mig_...
Content-Type: application/json
```

The body may be empty or `{}`. If chunks are missing, finalize returns `409`.
If another finalize is already importing the session, it returns `409`. Once the
session is done, a repeated finalize call returns the completed job response.

```json theme={null}
{
  "job_id": "job_abc123",
  "state": "done",
  "counts": {
    "keywords": 650,
    "keywords_created": 640,
    "keywords_skipped": 10,
    "history": 12000,
    "history_received": 12000
  }
}
```

## Crash Resume

Persist `session_id`, `chunk_count`, and every chunk checksum on the client.
After a client crash, re-upload all chunks whose success was not recorded. The
server accepts already stored chunks when the checksum matches.

If the connection drops during finalize, call finalize again. A `200` means the
session completed. A `409` means chunks are still missing or a finalize is still
running. Stale `receiving` and `importing` jobs are marked failed by maintenance
after 30 minutes, after which a new migration token and session are required.

## Status Codes

| Status | Meaning                                                                                                                                                                        |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `200`  | Chunk accepted, session finalized, or compatibility returned.                                                                                                                  |
| `201`  | Single request import completed or session created.                                                                                                                            |
| `400`  | Invalid JSON, invalid manifest, invalid chunk, or invalid index.                                                                                                               |
| `401`  | Missing or malformed migration token.                                                                                                                                          |
| `409`  | Live session conflict, chunk checksum conflict, missing chunks, already finalizing, or self-import (package exported from the destination project itself, code `self_import`). |
| `413`  | Request body or buffered session payload is too large.                                                                                                                         |
| `419`  | Migration token is invalid, expired, or already used.                                                                                                                          |
| `423`  | Destination project is in migration hold.                                                                                                                                      |
| `429`  | Rate limit exceeded.                                                                                                                                                           |
| `500`  | Import failed after the job started.                                                                                                                                           |

## Environment Knobs

| Variable                                     | Default   | Effect                                                                                                                  |
| -------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------- |
| `BISIBILITY_MIGRATION_IMPORT_MAX_BODY_BYTES` | `8388608` | Maximum JSON body size for single request imports and session requests.                                                 |
| `BISIBILITY_MIGRATION_TOKEN_TTL_MINUTES`     | `60`      | Migration token lifetime in minutes, clamped between `5` and `1440`.                                                    |
| `BISIBILITY_MIGRATION_HOLD_TTL_HOURS`        | `24`      | Hours before maintenance releases an expired migration hold.                                                            |
| `BISIBILITY_MIGRATION_EXPORT_MAX_KEYWORDS`   | `500`     | Maximum keywords included in a downloadable migration package.                                                          |
| `BISIBILITY_MIGRATION_MAX_KEYWORDS`          | `50000`   | Maximum keywords allowed for direct migration push.                                                                     |
| `BISIBILITY_MIGRATION_TARGET_ALLOWLIST`      | empty     | Comma-separated hostnames allowed as migration targets, with `*.suffix` wildcard support. Empty allows any public host. |

## Existing App Surfaces

The app also implements cloud-import state with server actions:

| Function                   | Purpose                                                              |
| -------------------------- | -------------------------------------------------------------------- |
| `mintMigrationToken`       | Create a single-use migration token for a project.                   |
| `regenerateMigrationToken` | Consume any active token and create a replacement.                   |
| `revokeMigrationToken`     | Consume an active migration token.                                   |
| `verifyMigrationToken`     | Verify and consume a raw migration token.                            |
| `createCloudImportJob`     | Move an import job to `receiving`.                                   |
| `advanceCloudImportJob`    | Advance a job through `receiving`, `importing`, `done`, or `failed`. |

## In-App Wizard

Open `/cloud/import`, generate or paste a migration token, then upload the JSON
package from the wizard. The wizard uses the same import handlers as the REST
endpoints.
