# Import

> The Import operations of the Hub REST API (4 operations).

URL: https://docs.synap.live/docs/reference/hub-api/import · Generated from Synap-core/backend@c31e261ab (main)

Paths are relative to `/api/hub` on a pod.

## POST /import/analyze

Structure import items into a preview graph

AI/deterministic structure of markdown/obsidian/csv/bookmark items into composite operations + a governed import.graph proposal. Pair with POST /import/apply (proposalId is SSOT for ops).

Request body (`application/json`): ImportRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `userId` | string | yes |  |
| `workspaceId` | string |  |  |
| `source` | "obsidian" \| "markdown" \| "csv" \| "bookmark" | yes |  |
| `relationType` | string |  |  |
| `aiStructure` | boolean |  |  |
| `items` | array<object> | yes |  |
| `sessionId` | string |  |  |
| `projectId` | string \| null |  |  |
| `forceSession` | boolean |  |  |
| `previewOnly` | boolean |  |  |

Responses:

- `200`: Operations + proposalId for preview-before-apply
- `400`: Bad request
- `403`: Forbidden
- `500`: Internal error

## POST /import/apply

Materialize a previewed import graph

Creates entities + relations from /import/analyze. When proposalId is set the stored proposal ops are SSOT (HITL); otherwise client operations (min 1) are required.

Request body (`application/json`): ImportApplyRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `userId` | string | yes |  |
| `workspaceId` | string |  |  |
| `source` | "obsidian" \| "markdown" \| "csv" \| "bookmark" | yes |  |
| `operations` | array<object> |  |  |
| `sessionId` | string |  |  |
| `projectId` | string \| null |  |  |
| `idempotencyKey` | string |  |  |
| `proposalId` | string |  |  |

Responses:

- `200`: Created + linked counts
- `400`: Bad request
- `403`: Forbidden
- `500`: Internal error

## POST /import/enqueue-corpus

Enqueue a large corpus import as a background job

Background door onto ImportOrchestrator.analyzeLarge: enqueues the corpus on the `import-corpus` pg-boss queue and returns immediately with a jobId. The worker chunks the items (cross-chunk dedup preserved) and produces ONE governed import.graph proposal — so a many-file corpus is a single background job instead of N synchronous /import/analyze calls racing the request timeout. Poll GET /import/corpus-job/{jobId}; on completion the result is the import.graph proposal in the review inbox.

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | string |  |  |
| `source` | "obsidian" \| "markdown" \| "csv" \| "bookmark" | yes |  |
| `items` | array<object> | yes |  |
| `relationType` | string |  |  |
| `aiStructure` | boolean |  |  |
| `sessionId` | string |  |  |
| `projectId` | string \| null |  |  |
| `forceSession` | boolean |  |  |
| `playbookId` | string |  |  |
| `playbookParams` | object |  |  |
| `previewOnly` | boolean |  |  |
| `userId` | string |  |  |

Responses:

- `202`: Corpus queued
- `400`: Bad request
- `403`: Forbidden
- `500`: Internal error

## GET /import/corpus-job/{jobId}

Poll a background corpus-import job

Returns the pg-boss state of a job created by POST /import/enqueue-corpus. Only the job's own submitter may read it. `completed` means the governed import.graph proposal has been written — fetch it from the proposals inbox. `output` carries the run's file-level outcome (proposalId, filesProcessed/filesFailed, warn+blocker findings); it is null while the job is unfinished and on pods older than the worker that returns it — treat null as UNKNOWN, never as success.

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `jobId` | path | string | yes |  |

Responses:

- `200`: Job state
- `400`: Malformed jobId
- `403`: Forbidden
- `404`: No such job for this caller
- `500`: Internal error
