# Imports

Start, follow and resume an import into one environment of a project (see [Importing an app](../apps/importing.md) for what comes across). The CLI's `flatstack import` and the MCP tools `import_project` and `get_import_status` use these routes.

An import writes your app users' data, so starting or resuming one needs `data:write` (owners, admins, and tokens with that scope); following one needs `data:read`.

| Request | Needs | What it does |
| --- | --- | --- |
| `POST /v1/projects/{projectId}/imports` | `data:write` | Starts an import, or with `dryRun: true` returns its report and writes nothing. |
| `GET /v1/projects/{projectId}/imports` | `data:read` | `{ imports }`: the latest 20, newest first. |
| `GET /v1/projects/{projectId}/imports/{importId}` | `data:read` | The import's status and progress. |
| `GET /v1/projects/{projectId}/imports/{importId}/report` | `data:read` | `{ status, report, files }`. `?format=markdown` returns the migration report alone. |
| `POST /v1/projects/{projectId}/imports/{importId}/resume` | `data:write` | Runs the next step. A completed import is returned as it is. |

## Starting

```json
{
  "source": "supabase",
  "environment": "production",
  "dryRun": true,
  "credentials": {
    "databaseUrl": "postgresql://postgres.abcd:password@aws-0-eu-west-2.pooler.supabase.com:5432/postgres",
    "projectUrl": "https://abcd.supabase.co",
    "serviceRoleKey": "eyJ...",
    "s3": { "accessKeyId": "...", "secretAccessKey": "...", "region": "eu-west-2" }
  },
  "functions": [{ "name": "send-digest", "files": [{ "path": "index.ts", "content": "..." }] }]
}
```

- `source`: `supabase`, `lovable` or `clerk` (below). `environment` defaults to `production`.
- `credentials.databaseUrl` is required; `projectUrl` with `serviceRoleKey`, or `s3`, lets the import copy files.
- `functions`: the edge functions in your repo's `supabase/functions/`, copied to `functions/legacy/`.

A dry run answers `200` with `{ dryRun: true, engine, blocked, summary, issues, report, files }`. `files` is everything the import writes to your repo, `MIGRATION_REPORT.md` included; the real run's report is the same, word for word.

A real run answers `201` with the import once its first step finished or failed, or `202` while it's still running. It copies for about 20 seconds per request.

```json
{
  "dryRun": false,
  "id": "imp_…",
  "source": "supabase",
  "environment": "production",
  "engine": "d1",
  "status": "running",
  "phase": "data",
  "position": "copying table todos",
  "progress": { "users": 4, "usersSkipped": 0, "signInLinks": 4, "rows": { "profiles": 3 }, "objects": 0, "objectsSkipped": 0, "bytes": 0 },
  "summary": { "tables": 4, "rows": 10, "users": 4, "objects": 2 },
  "review": 14,
  "error": null,
  "canResume": true
}
```

`status` is `running`, `completed` or `failed`. `phase` is `schema`, `users`, `data`, `foreign_keys`, `files` or `done`. A failed import has `error: { code, message }`; resuming carries on from where it stopped. `usersSkipped` counts users whose email address already belonged to someone in the project; they aren't imported.

### Clerk

```json
{
  "source": "clerk",
  "dryRun": true,
  "credentials": {
    "secretKey": "sk_live_...",
    "passwords": [{ "userId": "user_2abc", "digest": "$2b$10$...", "hasher": "bcrypt" }],
    "webhooks": ["https://example.com/api/clerk-webhook"]
  }
}
```

- `credentials.secretKey` is required: the Clerk application's secret key.
- `passwords`: the `id`, `password_digest` and `password_hasher` columns of the user export CSV from Clerk's dashboard. Only `bcrypt` hashes are kept. `flatstack import clerk --export <file.csv>` reads the file for you.
- `webhooks`: your Clerk webhook endpoints, for the report.

A Clerk import's report file is `MIGRATION_REPORT_CLERK.md`. Its `phase` is `schema`, `users`, `owners`, `organizations`, `memberships` or `done`, and its `progress` is `{ users, usersMerged, usersSkipped, signInLinks, ownerIdsRewritten, organizations, memberships }`. In the `owners` phase the import changes merged users' Clerk IDs (`user_…`) to their Flatstack IDs in your own tables' text columns; `ownerIdsRewritten` counts the rows changed, and the report lists each column. `usersMerged` counts Clerk users merged into a user already in the project with the same verified email; `usersSkipped` counts those whose matching address was unverified on one side, who aren't imported.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `validation_failed` | The body doesn't match. Credential values are never echoed back. |
| 409 | `import_running` | Another import into the environment is running, or another request is running this one's step. |
| 409 | `import_expired` | The import failed more than a day ago, so its credentials were deleted. Start it again; nothing is copied twice. |
| 422 | `import_blocked` | Something stops the import, listed in `details.problems`: a table that already exists, or a column D1 can't hold. Nothing was written. |
| 502 | `import_source_unreachable` | The source database couldn't be read. `details.error` has the reason, with credentials removed. |

Starting, planning, resuming, finishing and failing are written to the audit log (`import.started`, `import.planned`, `import.resumed`, `import.completed`, `import.failed`), without credentials. A project in another account, or an environment a deploy token wasn't made for, answers `404 not_found`.