{ "openapi": "3.1.0", "info": { "title": "Vern Migration API", "version": "1.0.0", "description": "Run Vern as a headless migration engine. Create a migration, upload a customer's files, run the managed agent to generate a preview, then execute and export clean data. See the Migration API guides for concepts and walkthroughs." }, "servers": [ { "url": "https://app.vern.so/api/v1", "description": "US-hosted accounts" }, { "url": "https://au.app.vern.so/api/v1", "description": "Australia-hosted accounts" } ], "security": [ { "apiKey": [] } ], "tags": [ { "name": "Catalog", "description": "Discover the sources and templates you set up in the Vern UI." }, { "name": "Migrations", "description": "Create a migration workspace and upload a customer's files." }, { "name": "Runs", "description": "Drive the managed agent: generate, refine, execute, answer, and observe." }, { "name": "Export", "description": "Download a migration's validated data as CSV." }, {"name":"Log","description":"The settled record of what a migration did."}, {"name":"Knowledge","description":"Durable context the agent reads before every run."}, {"name":"Exports","description":"Download imported data."} ], "paths": { "/sources": { "get": { "operationId": "listSources", "summary": "List sources", "description": "Returns the sources in your account — the systems you migrate customers off of. Sources are addressed by **name** throughout the API; no source ID is exposed.", "tags": ["Catalog"], "responses": { "200": { "description": "The sources in your account.", "content": { "application/json": { "schema": { "type": "object", "properties": { "sources": { "type": "array", "items": { "$ref": "#/components/schemas/Source" } } } }, "example": { "sources": [ { "name": "Salesforce", "logo_url": "https://logo.clearbit.com/salesforce.com" } ] } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/sources/{source}/pin": { "post": { "operationId": "pinSource", "summary": "Pin a source's recipe", "description": "Pin the recipe a source reuses as the starting point for future `generate` runs, so one customer's run can't drift the baseline others draw from. Pass `run_id: null` to unpin.", "tags": ["Catalog"], "parameters": [ { "name": "source", "in": "path", "required": true, "description": "The source name (URL-encode spaces).", "schema": { "type": "string" }, "example": "Salesforce" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "run_id": { "type": ["string", "null"], "format": "uuid", "description": "The run whose recipe to pin, or null to unpin." } }, "required": ["run_id"] }, "example": { "run_id": "d4c3b2a1-9f8e-47d6-b5a4-c3d2e1f0a9b8" } } } }, "responses": { "200": { "description": "The updated pin.", "content": { "application/json": { "schema": { "type": "object", "properties": { "source": { "type": "string" }, "pinned_recipe_run_id": { "type": ["string", "null"], "format": "uuid" } } }, "example": { "source": "Salesforce", "pinned_recipe_run_id": "d4c3b2a1-9f8e-47d6-b5a4-c3d2e1f0a9b8" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/templates": { "get": { "operationId": "listTemplates", "summary": "List templates", "description": "Returns the active templates in your account — the importable objects, each with its columns and validation rules. Templates are addressed by **slug**.", "tags": ["Catalog"], "responses": { "200": { "description": "The active templates in your account.", "content": { "application/json": { "schema": { "type": "object", "properties": { "templates": { "type": "array", "items": { "$ref": "#/components/schemas/Template" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/templates/{template_slug}": { "get": { "operationId": "getTemplate", "summary": "Get a template", "description": "Fetch one active template by its slug.", "tags": ["Catalog"], "parameters": [ { "name": "template_slug", "in": "path", "required": true, "schema": { "type": "string" }, "example": "contacts" } ], "responses": { "200": { "description": "The template.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Template" } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } }, "patch": { "operationId": "updateTemplate", "summary": "Update a template", "description": "Partial update of an active template. The `slug` is frozen — renaming changes only the display name, never the address. `columns` is a full replacement of the column set when present; cross-cell rules stored on the template are always preserved. Edits are refused with `409` while a migration that uses the template is running. `grouping` organises the template list in the dashboard and is not returned by template reads.", "tags": ["Catalog"], "parameters": [ { "name": "template_slug", "in": "path", "required": true, "schema": { "type": "string" }, "example": "contacts" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "minProperties": 1, "properties": { "name": { "type": "string", "description": "New display name. Does not change the slug." }, "description": { "type": ["string", "null"], "description": "The template's knowledge / description. `null` clears it." }, "grouping": { "type": ["string", "null"], "description": "Free-text group label for the dashboard. `null` clears it." }, "columns": { "type": "array", "minItems": 1, "description": "Full replacement of the column set.", "items": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "required": { "type": "boolean" }, "unique": { "type": "boolean" }, "desiredRule": { "type": "string" }, "strictRule": { "type": "string", "description": "Regex pattern for strict validation." } } } } } }, "example": { "description": "Suppliers master list. One row per active supplier.", "columns": [ { "name": "Supplier Name", "description": "Legal entity name", "required": true }, { "name": "ABN", "description": "Australian Business Number", "required": false, "desiredRule": "strict", "strictRule": "^\\d{11}$" } ] } } } }, "responses": { "200": { "description": "The updated template.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Template" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/TemplateNotFound" }, "409": { "$ref": "#/components/responses/TemplateLocked" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/migrations": { "post": { "operationId": "createMigration", "summary": "Create a migration", "description": "Create a migration workspace for one customer — a workbook with one sheet per template. Requires API-key auth specifically. `external_id` makes creation idempotent: re-sending the same one (with the same source) returns the existing migration with a 200.", "tags": ["Migrations"], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string", "description": "Display name for the migration (e.g. the customer's org name)." }, "source": { "type": "string", "description": "The source name this migration comes from. Optional — omit for a sourceless migration." }, "external_id": { "type": "string", "maxLength": 255, "pattern": "^[a-zA-Z0-9_-]+$", "description": "Your own identifier for this customer. Alphanumeric, hyphens, underscores. Makes creation idempotent." }, "templates": { "type": "array", "items": { "type": "string" }, "description": "Template slugs to create sheets from. Omit to use every active template." } } }, "example": { "name": "Acme Corp", "source": "Salesforce", "external_id": "acme-001", "templates": ["contacts"] } } } }, "responses": { "201": { "description": "The created migration (200 when an existing migration is returned via external_id idempotency).", "content": { "application/json": { "schema": { "type": "object", "properties": { "migration": { "$ref": "#/components/schemas/Migration" } } }, "example": { "migration": { "id": "c0a8012e-4f1b-4d3a-9b2c-7e6f5a4b3c2d", "name": "Acme Corp", "source": "Salesforce", "status": "awaiting_files", "templates": [ { "slug": "contacts", "name": "Contacts" } ] } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/migrations/{migration_id}/files": { "parameters": [ { "$ref": "#/components/parameters/MigrationId" } ], "post": { "operationId": "createUploads", "summary": "Mint signed upload URLs", "description": "Request one signed upload URL per file, then PUT each file's bytes to its `signed_url`. Files are workbook-level — not mapped to templates.", "tags": ["Migrations"], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["files"], "properties": { "files": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string" }, "content_type": { "type": "string" } } } } } }, "example": { "files": [ { "name": "contacts.csv", "content_type": "text/csv" } ] } } } }, "responses": { "201": { "description": "One signed upload per requested file.", "content": { "application/json": { "schema": { "type": "object", "properties": { "files": { "type": "array", "items": { "$ref": "#/components/schemas/SignedUpload" } } } }, "example": { "files": [ { "name": "contacts.csv", "storage_path": "workbooks/b3d9.../-contacts.csv", "signed_url": "https://storage.vern.so/object/upload/sign/...&token=...", "token": "ey...", "content_type": "text/csv" } ] } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } }, "get": { "operationId": "listFiles", "summary": "List uploaded files", "description": "List the customer-uploaded files for the migration.", "tags": ["Migrations"], "responses": { "200": { "description": "The uploaded files.", "content": { "application/json": { "schema": { "type": "object", "properties": { "files": { "type": "array", "items": { "$ref": "#/components/schemas/ListedFile" } } } }, "example": { "files": [ { "name": "contacts.csv", "storage_path": "workbooks/b3d9.../-contacts.csv", "size": 184320, "content_type": "text/csv", "uploaded_at": "2026-06-19T15:30:02.000Z" } ] } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/migrations/{migration_id}/runs": { "parameters": [ { "$ref": "#/components/parameters/MigrationId" } ], "post": { "operationId": "startRun", "summary": "Start a run", "description": "Start one managed-agent run. `generate` authors the recipe and stops at a preview; `update` edits it per `message`; `clarify` asks a read-only question; `execute` runs the approved preview for real. `generate` opens a preview run and `update`/`clarify`/`execute` resume that same run, so the `run_id` stays stable from preview through execution (a resuming call returns `status: \"running\"`). Only one run runs at a time per migration (409 with the active run_id otherwise).", "tags": ["Runs"], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["kind"], "properties": { "kind": { "type": "string", "enum": ["generate", "update", "clarify", "execute"] }, "message": { "type": "string", "description": "Required for update and clarify — your instruction or question in plain language." }, "extract_via_api": { "type": "boolean", "description": "generate only. Controls live API extraction from a connected source: true extracts via the connection (alongside any files), false forces file-only, omitted extracts via API only when no files were uploaded and a connection exists." }, "instructions": { "type": ["string", "null"], "maxLength": 200, "description": "generate only. Optional guidance for this migration (at most 200 characters), e.g. \"Only import active customers\". The agent follows it but it cannot override Vern's rules, a locked template or the Memory setting. Kept for every later leg of the run and returned on the run as `instructions`. Longer than 200 characters returns 400 with code instructions_too_long." } } }, "examples": { "generate": { "summary": "Generate a preview", "value": { "kind": "generate" } }, "generate_api": { "summary": "Generate from a live source", "value": { "kind": "generate", "extract_via_api": true } }, "generate_instructions": { "summary": "Generate with instructions", "value": { "kind": "generate", "instructions": "Only import active customers and keep their original IDs." } }, "update": { "summary": "Refine the recipe", "value": { "kind": "update", "message": "Trim whitespace from every phone number." } }, "clarify": { "summary": "Ask a read-only question", "value": { "kind": "clarify", "message": "Which column did you map Email from?" } }, "execute": { "summary": "Execute for real", "value": { "kind": "execute" } } } } } }, "responses": { "201": { "description": "The run was accepted. A `generate` is `queued`; a resuming `update`/`clarify`/`execute` is `running`. Poll `poll_url`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RunAccepted" }, "example": { "run_id": "d4c3b2a1-9f8e-47d6-b5a4-c3d2e1f0a9b8", "status": "queued", "poll_url": "https://app.vern.so/api/v1/migrations/c0a8012e-.../runs/d4c3b2a1-9f8e-47d6-b5a4-c3d2e1f0a9b8" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "description": "A run is already in progress (the body carries the active run_id), there is no preview to act on / execute, or the approved preview is no longer resumable (generate a fresh one).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "A run is already in progress for this migration.", "run_id": "d4c3b2a1-9f8e-47d6-b5a4-c3d2e1f0a9b8" } } } }, "429": { "$ref": "#/components/responses/RunStartLimited" } } } }, "/migrations/{migration_id}/runs/{run_id}": { "parameters": [ { "$ref": "#/components/parameters/MigrationId" }, { "$ref": "#/components/parameters/RunId" } ], "get": { "operationId": "getRun", "summary": "Poll a run", "description": "Read a run's state. Every response carries `run_id`, `status`, and `created_at` (plus `answers`, a read-back of your last reply, once you've answered a block); the rest depends on status (`message` while running, `blocked_reason` + `questions`/`credential_request` when blocked, `report` on a completed execute, `error` on failure).", "tags": ["Runs"], "responses": { "200": { "description": "The run's current state.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Run" }, "examples": { "running": { "summary": "running", "value": { "run_id": "d4c3b2a1-...", "status": "running", "created_at": "2026-06-19T15:32:11.000Z", "message": "Cleaning your data" } }, "blocked": { "summary": "blocked (question)", "value": { "run_id": "d4c3b2a1-...", "status": "blocked", "created_at": "2026-06-19T15:32:11.000Z", "blocked_reason": "question", "questions": [ { "id": "q1", "question": "Some Status values aren't one of the allowed options. How should I handle them?", "options": [ { "id": "a", "label": "Map them all to 'Open'" }, { "id": "b", "label": "Drop those rows" } ], "allowCustom": true } ] } }, "blocked_credentials": { "summary": "blocked (credentials)", "value": { "run_id": "d4c3b2a1-...", "status": "blocked", "created_at": "2026-06-19T15:32:11.000Z", "blocked_reason": "credentials", "credential_request": { "connection_id": "9b1f2c3d-...", "source_key": "salesforce", "source_name": "Salesforce", "reason": "I need API access to read your contacts.", "schema": { "client_id": { "type": "string" }, "client_secret": { "type": "string", "secret": true } } } } }, "awaiting_approval": { "summary": "awaiting_approval", "value": { "run_id": "d4c3b2a1-...", "status": "awaiting_approval", "created_at": "2026-06-19T15:32:11.000Z" } }, "completed": { "summary": "completed (execute)", "value": { "run_id": "e5d4c3b2-...", "status": "completed", "created_at": "2026-06-19T15:40:02.000Z", "report": { "inserted": 1450, "perSheet": [ { "templateName": "Contacts", "sheetId": "7a1c2b3d-...", "rowCount": 1450 } ], "invalidCellCount": 12, "coverage": null } } }, "failed": { "summary": "failed", "value": { "run_id": "d4c3b2a1-...", "status": "failed", "created_at": "2026-06-19T15:32:11.000Z", "error": "Run failed or timed out" } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } }, "patch": { "operationId": "cancelRun", "summary": "Cancel a run", "description": "Cancel an in-flight run. The only accepted body is `{ \"status\": \"cancel_requested\" }`.", "tags": ["Runs"], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["status"], "properties": { "status": { "type": "string", "enum": ["cancel_requested"] } } }, "example": { "status": "cancel_requested" } } } }, "responses": { "200": { "description": "The run in its updated (canceled) shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Run" }, "example": { "run_id": "d4c3b2a1-...", "status": "canceled", "created_at": "2026-06-19T15:32:11.000Z" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/migrations/{migration_id}/runs/{run_id}/messages": { "parameters": [ { "$ref": "#/components/parameters/MigrationId" }, { "$ref": "#/components/parameters/RunId" } ], "post": { "operationId": "answerRun", "summary": "Answer a blocked run", "description": "Resume a run paused at `blocked` with `blocked_reason: \"question\"`. Send `answers` as the structured `[{id,value}]` form (one per question) or a free-form string. Only a question-blocked run accepts this (409 otherwise) — a credentials-blocked run is rejected so secrets never enter the agent thread; submit those to `POST .../credentials` instead.", "tags": ["Runs"], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["answers"], "properties": { "answers": { "description": "Structured answers (one {id,value} per question) or a free-form string.", "oneOf": [ { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "value": { "type": "string" } } } }, { "type": "string" } ] } } }, "examples": { "structured": { "summary": "Structured", "value": { "answers": [ { "id": "q1", "value": "Treat CANCELLED as inactive" } ] } }, "freeform": { "summary": "Free-form", "value": { "answers": "Use the company domain for any missing Email, don't drop the row." } } } } } }, "responses": { "200": { "description": "The run resumed and is running again.", "content": { "application/json": { "schema": { "type": "object", "properties": { "run_id": { "type": "string", "format": "uuid" }, "status": { "type": "string", "example": "running" } } }, "example": { "run_id": "d4c3b2a1-9f8e-47d6-b5a4-c3d2e1f0a9b8", "status": "running" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/migrations/{migration_id}/runs/{run_id}/credentials": { "parameters": [ { "$ref": "#/components/parameters/MigrationId" }, { "$ref": "#/components/parameters/RunId" } ], "post": { "operationId": "submitCredentials", "summary": "Submit source credentials", "description": "Resume a run paused at `blocked` with `blocked_reason: \"credentials\"`. The secrets are stored in a secured vault and never enter the agent thread, the run row, or logs. The target connection is taken from the run's own `credential_request` — a key can only fill the connection the agent asked about. Re-submitting overwrites the stored secret so a rejected credential can be re-entered.", "tags": ["Runs"], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["credentials"], "properties": { "credentials": { "type": "object", "description": "Secret values keyed to the request's schema. Must be a non-empty object.", "additionalProperties": true }, "connection_id": { "type": "string", "format": "uuid", "description": "Optional guard — if sent, must match the run's credential_request.connection_id." } } }, "example": { "credentials": { "client_id": "…", "client_secret": "…", "refresh_token": "…" } } } } }, "responses": { "200": { "description": "Credentials stored; the run resumed and is running again.", "content": { "application/json": { "schema": { "type": "object", "properties": { "run_id": { "type": "string", "format": "uuid" }, "status": { "type": "string", "example": "running" } } }, "example": { "run_id": "d4c3b2a1-9f8e-47d6-b5a4-c3d2e1f0a9b8", "status": "running" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "description": "The run isn't awaiting credentials (not blocked, or blocked on a question), connection_id doesn't match, or there's no active credential request yet — retry shortly.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "This run is not awaiting credentials." } } } }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/migrations/{migration_id}/source-connection": { "parameters": [ { "$ref": "#/components/parameters/MigrationId" } ], "get": { "operationId": "getSourceConnection", "summary": "Get the source connection", "description": "Read the migration's live API source connection, or 404 if none is configured. Never returns secret values — `creds_configured` reports whether credentials are on file.", "tags": ["Migrations"], "parameters": [ { "name": "source_key", "in": "query", "required": false, "description": "Filter to a specific connector.", "schema": { "type": "string" }, "example": "salesforce" }, { "name": "profile_id", "in": "query", "required": false, "description": "Profile to read; pass the literal `workbook` for the workbook-bound connection.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "The current source connection.", "content": { "application/json": { "schema": { "type": "object", "properties": { "source_connection": { "$ref": "#/components/schemas/SourceConnection" } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } }, "post": { "operationId": "saveSourceConnection", "summary": "Create or update the source connection", "description": "Attach a live API source to the migration, or update it in place. `credentials` (optional here) go to a secured vault and are never returned; you can also supply them later via Submit credentials.", "tags": ["Migrations"], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["source_key"], "properties": { "source_key": { "type": "string", "description": "The connector to attach, lower-cased. Unknown or not-yet-enabled keys return 400." }, "profile_id": { "type": ["string", "null"], "description": "Scope to a profile; null binds to the workbook; omit for the default profile." }, "config": { "type": "object", "description": "Non-secret connector configuration.", "additionalProperties": true }, "selected_streams": { "type": ["array", "null"], "items": { "type": "string" }, "description": "Streams to extract, or null for connector default." }, "stream_state": { "type": "object", "description": "Incremental-sync cursor state.", "additionalProperties": true }, "credentials": { "type": "object", "description": "Secret values, vaulted and never returned.", "additionalProperties": true } } }, "example": { "source_key": "salesforce", "config": { "instance_url": "https://acme.my.salesforce.com" }, "selected_streams": ["contacts", "accounts"], "credentials": { "client_id": "…", "client_secret": "…" } } } } }, "responses": { "201": { "description": "The created or updated connection.", "content": { "application/json": { "schema": { "type": "object", "properties": { "source_connection": { "$ref": "#/components/schemas/SourceConnection" } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } }, "delete": { "operationId": "deleteSourceConnection", "summary": "Delete the source connection", "description": "Remove the migration's source connection and its stored secrets. Accepts the same optional source_key and profile_id query parameters as GET.", "tags": ["Migrations"], "parameters": [ { "name": "source_key", "in": "query", "required": false, "schema": { "type": "string" } }, { "name": "profile_id", "in": "query", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Whether a connection was deleted.", "content": { "application/json": { "schema": { "type": "object", "properties": { "deleted": { "type": "boolean" } } }, "example": { "deleted": true } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/migrations/{migration_id}/preview": { "parameters": [ { "$ref": "#/components/parameters/MigrationId" } ], "get": { "parameters": [ { "$ref": "#/components/parameters/Fields" }, { "$ref": "#/components/parameters/OmitEmpty" } ], "operationId": "getPreview", "summary": "Get the preview", "description": "The latest generated preview — the proposed output, sheet-per-template. 404 until a generate/update run has produced one.", "tags": ["Runs"], "responses": { "200": { "description": "The current preview.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Preview" }, "example": { "status": "awaiting_approval", "templates": [ { "slug": "contacts", "name": "Contacts" } ], "sheets": [ { "template": { "slug": "contacts", "name": "Contacts" }, "rows": [ ["Email", "First Name", "Last Name"], ["jane@acme.com", "Jane", "Doe"] ], "invalid_cells": [] } ] } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/migrations/{migration_id}/thread": { "parameters": [ { "$ref": "#/components/parameters/MigrationId" } ], "get": { "operationId": "getThread", "summary": "Get the thread (snapshot)", "description": "A snapshot of the agent's activity thread across all of the migration's runs, ordered by a stable cross-run sequence. Page forward by passing the previous `last_sequence` as `after`.", "tags": ["Runs"], "parameters": [ { "name": "after", "in": "query", "required": false, "description": "Return only events with sequence greater than this.", "schema": { "type": "integer" } }, { "name": "include", "in": "query", "required": false, "description": "Verbosity.", "schema": { "type": "string", "enum": ["user", "tools", "all"], "default": "user" } } ], "responses": { "200": { "description": "A page of thread events.", "content": { "application/json": { "schema": { "type": "object", "properties": { "events": { "type": "array", "items": { "$ref": "#/components/schemas/ThreadEvent" } }, "last_sequence": { "type": ["integer", "null"] } } }, "example": { "events": [ { "sequence": 1042, "type": "status", "data": { "message": "Reading your files" }, "created_at": "2026-06-19T15:32:13.000Z" } ], "last_sequence": 1042 } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/migrations/{migration_id}/thread/stream": { "parameters": [ { "$ref": "#/components/parameters/MigrationId" } ], "get": { "operationId": "streamThread", "summary": "Stream the thread (SSE)", "description": "A Server-Sent Events stream of the same thread events. Each message is `event: ` + `data: `. The stream ends with `event: done` and closes after ~5 minutes; reconnect with `?after=` to continue.", "tags": ["Runs"], "parameters": [ { "name": "after", "in": "query", "required": false, "schema": { "type": "integer" } }, { "name": "include", "in": "query", "required": false, "schema": { "type": "string", "enum": ["user", "tools", "all"], "default": "user" } } ], "responses": { "200": { "description": "An SSE stream.", "content": { "text/event-stream": { "schema": { "type": "string" } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/migrations/{migration_id}/exports/{file}": { "parameters": [ { "$ref": "#/components/parameters/MigrationId" } ], "get": { "operationId": "exportCsv", "summary": "Export a sheet as CSV", "description": "Stream one sheet's data as a CSV, addressed by `{template_slug}.csv`. Every row is included. The body is streamed (gzip-compressed when the request advertises it) and an `X-Total-Records` header reports the total count.", "tags": ["Export"], "parameters": [ { "name": "file", "in": "path", "required": true, "description": "The template slug with a .csv suffix, e.g. contacts.csv.", "schema": { "type": "string" }, "example": "contacts.csv" }, { "$ref": "#/components/parameters/Fields" }, { "$ref": "#/components/parameters/OmitEmpty" } ], "responses": { "200": { "description": "The CSV stream.", "content": { "text/csv": { "schema": { "type": "string", "format": "binary" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/migrations/{migration_id}/log": { "parameters": [{"$ref":"#/components/parameters/MigrationId"}], "get": { "tags": ["Log"], "summary": "Get the session log", "description": "The settled record of a migration: what was planned, which files went\nin, who approved what, how many rows were written, and whether existing\nrows were replaced.\n\nIntended to be fetched once and stored, so you can explain a migration\nto your own customer later. It does **not** include the conversation\ntranscript — that is `/thread` — and it does not include individual row\nvalues.\n\nAvailable once nothing is in flight, including for migrations that\nfailed: a run can write rows and then fail, which is exactly when this\nmatters. While a run is active the endpoint returns `409`.\n\nPass `?format=md` for the same record as a readable Markdown report.\n\nA migration can be imported again later, so the log always covers every\nrun to date — a re-fetch is a superset of an earlier one. Use\n`generated_at` to tell snapshots apart.\n", "operationId": "getSessionLog", "parameters": [ { "name": "format", "in": "query", "schema": {"type":"string","enum":["json","md","markdown"],"default":"json"} } ], "responses": { "200": { "description": "The session log.", "content": { "application/json": {"schema":{"$ref":"#/components/schemas/SessionLog"}}, "text/markdown": {"schema":{"type":"string"}} } }, "401": {"$ref":"#/components/responses/Unauthorized"}, "404": {"$ref":"#/components/responses/MigrationNotFound"}, "409": { "description": "The migration is still in progress. Body carries the current `status` and `retry_after_seconds`.", "content": { "application/json": { "schema": { "allOf": [ {"$ref":"#/components/schemas/Error"}, { "type": "object", "properties": { "status": {"$ref":"#/components/schemas/MigrationStatus"}, "retry_after_seconds": {"type":"integer","example":30} } } ] } } } }, "429": {"$ref":"#/components/responses/RateLimited"}, "500": {"$ref":"#/components/responses/ServerError"}, "503": { "description": "The log could not be assembled. Retryable.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} } } }, "options": { "tags": ["Log"], "summary": "CORS preflight", "security": [], "responses": {"204":{"description":"No content."}} } }, "/migrations/{migration_id}/knowledge": { "parameters": [{"$ref":"#/components/parameters/MigrationId"}], "get": { "tags": ["Knowledge"], "summary": "Read customer knowledge", "description": "Durable facts about this customer that the agent honours on every run — \"amounts are always USD\", \"skip internal test records\". Distinct from source knowledge, which is about the source software itself.", "operationId": "getMigrationKnowledge", "responses": { "200": { "description": "The stored knowledge.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/MigrationKnowledge"}}} }, "401": {"$ref":"#/components/responses/Unauthorized"}, "404": {"$ref":"#/components/responses/MigrationNotFound"}, "429": {"$ref":"#/components/responses/RateLimited"}, "500": {"$ref":"#/components/responses/ServerError"} } }, "patch": { "tags": ["Knowledge"], "summary": "Replace customer knowledge", "description": "`knowledge` **replaces** the whole document; an empty string clears it.\nThe agent rewrites this field the same way, so read, merge, then write.\n\nPass `if_unmodified_since` with the `updated_at` you read to make that\nsafe: if it changed underneath you the write is refused with `412`\nrather than overwriting.\n", "operationId": "updateMigrationKnowledge", "requestBody": { "required": true, "content": {"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeWrite"}}} }, "responses": { "200": { "description": "Knowledge updated.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/MigrationKnowledge"}}} }, "400": {"$ref":"#/components/responses/BadRequest"}, "401": {"$ref":"#/components/responses/Unauthorized"}, "404": {"$ref":"#/components/responses/MigrationNotFound"}, "409": {"$ref":"#/components/responses/KnowledgeLocked"}, "412": {"$ref":"#/components/responses/KnowledgeStale"}, "429": {"$ref":"#/components/responses/RateLimited"}, "500": {"$ref":"#/components/responses/ServerError"} } }, "options": { "tags": ["Knowledge"], "summary": "CORS preflight", "security": [], "responses": {"204":{"description":"No content."}} } }, "/migrations/{migration_id}/exports": { "parameters": [{"$ref":"#/components/parameters/MigrationId"}], "post": { "tags": ["Exports"], "summary": "Start an export", "description": "Queues an export of one or more destination sheets. One slug produces a CSV, several produce a ZIP. Poll the returned `status_url` for the download link.", "operationId": "createExport", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "slug":{"type":"string"}, "slugs":{"type":"array","items":{"type":"string"}}, "fields":{"type":"array","items":{"type":"string"},"description":"Column names to export, in the order given. Omit to export every column. Matched exactly and case-sensitively; an unrecognised name returns `400` rather than being ignored.","example":["Email","Company Name"]}, "omit_empty":{"type":"boolean","default":false,"description":"Drop columns with no data in any row, using the completed import's stored per-column coverage. Returns `409` (`coverage_unavailable`) when that coverage does not exist, rather than quietly exporting every column."} }, "description": "Provide `slug` or `slugs`; `slugs` wins if both are sent." } } } }, "responses": { "202": { "description": "Export queued.", "content": { "application/json": { "schema": { "type": "object", "properties": { "job_id": {"type":"string"}, "status": {"type":"string","enum":["pending"]}, "format": {"type":"string","enum":["csv","zip"]}, "total_rows": {"type":"integer"}, "status_url": {"type":"string","description":"Relative path of this job's status endpoint."} } } } } }, "400": {"$ref":"#/components/responses/BadRequest"}, "401": {"$ref":"#/components/responses/Unauthorized"}, "404": { "description": "Migration not found, or an unknown template slug.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} }, "409": { "description": "A sheet has no exportable data.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} }, "429": {"$ref":"#/components/responses/RateLimited"}, "500": {"$ref":"#/components/responses/ServerError"}, "502": { "description": "The export worker could not be started. Retryable.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} } } }, "options": { "tags": ["Exports"], "summary": "CORS preflight", "security": [], "responses": {"204":{"description":"No content."}} } }, "/migrations/{migration_id}/exports/jobs/{job_id}": { "parameters": [ {"$ref":"#/components/parameters/MigrationId"}, {"name":"job_id","in":"path","required":true,"schema":{"type":"string"}} ], "get": { "tags": ["Exports"], "summary": "Poll an export job", "operationId": "getExportJob", "responses": { "200": { "description": "Job state. `download_url` is set once `status` is `completed`.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/ExportJob"}}} }, "401": {"$ref":"#/components/responses/Unauthorized"}, "404": { "description": "Migration or job not found.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} }, "429": {"$ref":"#/components/responses/RateLimited"}, "500": {"$ref":"#/components/responses/ServerError"} } }, "options": { "tags": ["Exports"], "summary": "CORS preflight", "security": [], "responses": {"204":{"description":"No content."}} } }, "/sources/{source}": { "patch": { "operationId": "updateSource", "summary": "Update a source", "description": "Partial update of a source's `name` and/or `auth_url`. Renaming changes the name you address this source by on every subsequent call; in-flight migrations are unaffected. A rename that matches another source's exact name is refused — it would make both unaddressable. Source knowledge is managed separately at `/sources/{source}/knowledge`.", "tags": ["Catalog"], "parameters": [ { "name": "source", "in": "path", "required": true, "description": "The source name (URL-encode spaces).", "schema": { "type": "string" }, "example": "Salesforce" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "minProperties": 1, "properties": { "name": { "type": "string", "description": "New display name. Becomes the name you address this source by." }, "auth_url": { "type": "string", "description": "The source system's sign-in / portal URL." } } }, "example": { "name": "Salesforce (AU)", "auth_url": "https://login.salesforce.com" } } } }, "responses": { "200": { "description": "The updated source.", "content": { "application/json": { "schema": { "type": "object", "properties": { "source": { "type": "object", "properties": { "name": { "type": "string" }, "logo_url": { "type": ["string", "null"] }, "auth_url": { "type": "string" } } } } }, "example": { "source": { "name": "Salesforce (AU)", "logo_url": null, "auth_url": "https://login.salesforce.com" } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/SourceNotFound" }, "409": { "description": "The source name is ambiguous, or the new name is already used by another source in your organisation.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/sources/{source}/knowledge": { "parameters": [{"$ref":"#/components/parameters/SourceName"}], "get": { "tags": ["Knowledge"], "summary": "Read source knowledge", "description": "Durable facts about the source software itself — field meanings, enum codes, export quirks. Seeding this means a run does not relearn what you already know.", "operationId": "getSourceKnowledge", "responses": { "200": { "description": "The stored knowledge.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/SourceKnowledge"}}} }, "401": {"$ref":"#/components/responses/Unauthorized"}, "404": {"$ref":"#/components/responses/SourceNotFound"}, "409": {"$ref":"#/components/responses/AmbiguousSource"}, "429": {"$ref":"#/components/responses/RateLimited"}, "500": {"$ref":"#/components/responses/ServerError"} } }, "patch": { "tags": ["Knowledge"], "summary": "Replace source knowledge", "description": "`knowledge` replaces the whole document; an empty string clears it. Use `if_unmodified_since` for safe read-modify-write.", "operationId": "updateSourceKnowledge", "requestBody": { "required": true, "content": {"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeWrite"}}} }, "responses": { "200": { "description": "Knowledge updated.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/SourceKnowledge"}}} }, "400": {"$ref":"#/components/responses/BadRequest"}, "401": {"$ref":"#/components/responses/Unauthorized"}, "404": {"$ref":"#/components/responses/SourceNotFound"}, "409": { "description": "The source name is ambiguous, or a migration is currently running for this source and holds the knowledge lock.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} }, "412": {"$ref":"#/components/responses/KnowledgeStale"}, "429": {"$ref":"#/components/responses/RateLimited"}, "500": {"$ref":"#/components/responses/ServerError"} } }, "options": { "tags": ["Knowledge"], "summary": "CORS preflight", "security": [], "responses": {"204":{"description":"No content."}} } } }, "components": { "securitySchemes": { "apiKey": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Vern API key. Create one at Settings → API keys." } }, "parameters": { "MigrationId": { "name": "migration_id", "in": "path", "required": true, "description": "The migration ID returned by Create a migration.", "schema": { "type": "string", "format": "uuid" }, "example": "c0a8012e-4f1b-4d3a-9b2c-7e6f5a4b3c2d" }, "RunId": { "name": "run_id", "in": "path", "required": true, "description": "The run ID returned by Start a run.", "schema": { "type": "string", "format": "uuid" }, "example": "d4c3b2a1-9f8e-47d6-b5a4-c3d2e1f0a9b8" }, "SourceName": { "name": "source", "in": "path", "required": true, "schema": {"type":"string"}, "description": "The source's name, as returned by `GET /sources`. URL-encode names containing spaces.", "example": "HubSpot" }, "Fields": { "name": "fields", "in": "query", "required": false, "schema": {"type":"string"}, "description": "Comma-separated column names to return, in the order given. Omit to return every column. Names are matched exactly and are case-sensitive \u2014 they are the same names used as CSV headers. An unrecognised name returns `400` (`unknown_field`) rather than being ignored, so a typo cannot silently leave data out of the response.", "example": "Email,Company Name" }, "OmitEmpty": { "name": "omit_empty", "in": "query", "required": false, "schema": {"type":"boolean","default":false}, "description": "Drop columns that hold no data. A column is dropped only when it is empty in **every** row, so the rows you get back all keep the same shape. Empty means an empty string, `null`, or an absent value; whitespace counts as data. Combine with `fields` and the pruning applies within your selection.\n\nHow \"empty\" is decided depends on the endpoint. For exports it is read from the completed import's stored per-column coverage, so it reflects the **whole sheet** and stays stable across downloads; if that coverage is unavailable the request returns `409` (`coverage_unavailable`) rather than quietly returning every column. For the preview and extraction endpoints the returned rows are the entire payload, so emptiness is decided from them directly." } }, "responses": { "BadRequest": { "description": "Malformed request.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "Unauthorized": { "description": "API key missing, malformed, or revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "NotFound": { "description": "The referenced resource isn't in your account.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "Conflict": { "description": "A uniqueness or state conflict.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "RateLimited": { "description": "Rate limit hit. Counted per account, not per API key. Creating a migration and starting a `generate` run are limited to 30/hour each; answering a run is limited to 30/hour; reads allow 30/minute and writes 30/minute. `Retry-After` gives the seconds until the window resets, alongside `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.", "headers": { "Retry-After": { "description": "Seconds until the rate-limit window resets.", "schema": { "type": "integer" } }, "X-RateLimit-Limit": { "description": "Requests allowed in the current window.", "schema": { "type": "integer" } }, "X-RateLimit-Remaining": { "description": "Requests remaining in the current window.", "schema": { "type": "integer" } }, "X-RateLimit-Reset": { "description": "Unix time (seconds) when the window resets.", "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Too many requests" } } } }, "RunStartLimited": { "description": "Either the hourly rate limit on `kind: \"generate\"` (30/hour per account) or the concurrent-run limit (3 in progress). The concurrency refusal is identifiable by `code: \"run_concurrency_limit\"` and lists the blocking runs; a plain rate limit carries only `error` plus `Retry-After`.", "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/Error" }, { "$ref": "#/components/schemas/RunConcurrencyError" } ] } } } }, "RunConcurrencyLimited": { "description": "The account already has the maximum number of runs in progress (3), counting runs that are `queued`, `running` or `blocked`. Runs in `awaiting_approval` wait on you and do not count. Wait for one to finish or cancel one, then retry.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RunConcurrencyError" }, "example": { "error": "You already have 3 imports running, which is the limit. Wait for one to finish, or stop one to start another.", "code": "run_concurrency_limit", "limit": 3, "runs": [ { "id": "run_01JD...", "status": "running", "updatedAt": "2026-10-01T09:14:22.000Z", "runKind": "generate" } ] } } } }, "MigrationNotFound": { "description": "No migration with this id in your organisation. Returned rather than `403` so the existence of another organisation's migration is not disclosed.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} }, "ServerError": { "description": "Unexpected server error.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} }, "KnowledgeLocked": { "description": "A migration is currently running and may be rewriting this document.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} }, "KnowledgeStale": { "description": "The document changed after the `if_unmodified_since` you supplied. The body carries the current `updated_at`; re-read, merge, and retry.", "content": { "application/json": { "schema": { "allOf": [ {"$ref":"#/components/schemas/Error"}, {"type":"object","properties":{"updated_at":{"type":"string","nullable":true}}} ] } } } }, "SourceNotFound": { "description": "No source with this name in your organisation.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} }, "AmbiguousSource": { "description": "More than one source shares this name. Rename one so the reference is unambiguous.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} }, "TemplateNotFound": { "description": "No active template with this slug in your organisation.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} }, "TemplateLocked": { "description": "A migration that uses this template is currently running; edits are refused until it settles.", "content": {"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}} } }, "schemas": { "Error": { "type": "object", "properties": { "error": { "type": "string", "description": "A human-readable error message." } }, "required": ["error"] }, "RunConcurrencyError": { "type": "object", "description": "A `429` from the concurrent-run limit. Distinguished from a plain rate limit by the `code` field.", "properties": { "error": { "type": "string", "description": "A human-readable error message." }, "code": { "type": "string", "enum": ["run_concurrency_limit"], "description": "Machine-readable discriminator." }, "limit": { "type": "integer", "description": "The maximum number of runs allowed in progress at once." }, "runs": { "type": "array", "description": "The runs currently holding capacity. Cancel or wait for one of these.", "items": { "type": "object", "properties": { "id": { "type": "string" }, "status": { "type": "string", "description": "One of `queued`, `running`, `blocked`." }, "updatedAt": { "type": ["string","null"], "format": "date-time" }, "runKind": { "type": ["string","null"] } }, "required": ["id","status"] } } }, "required": ["error","code","runs"] }, "Source": { "type": "object", "properties": { "name": { "type": "string", "description": "The source's public identifier." }, "logo_url": { "type": ["string", "null"] } } }, "TemplateColumn": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string", "maxLength": 500, "description": "What the column holds. At most 500 characters for a new or changed value." }, "required": { "type": "boolean" }, "unique": { "type": "boolean" }, "desiredRule": { "type": "string", "enum": ["none", "ai", "strict", "link"] }, "strictRule": { "type": "string", "description": "Regex the value must match (when desiredRule is strict)." }, "linkRule": { "type": "array", "items": { "type": "object", "properties": { "templateId": { "type": "string" }, "columnName": { "type": "string" } } }, "description": "Cross-template references (when desiredRule is link)." } } }, "Template": { "type": "object", "properties": { "slug": { "type": "string", "description": "Stable public identifier." }, "name": { "type": "string" }, "description": { "type": ["string", "null"] }, "columns": { "type": "array", "items": { "$ref": "#/components/schemas/TemplateColumn" } } } }, "Migration": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "The migration ID — pass it to every later call." }, "name": { "type": "string" }, "source": { "type": ["string", "null"] }, "status": { "type": "string", "enum": ["awaiting_files", "ready", "awaiting_approval", "completed"] }, "templates": { "type": "array", "items": { "type": "object", "properties": { "slug": { "type": "string" }, "name": { "type": "string" } } } } } }, "SignedUpload": { "type": "object", "properties": { "name": { "type": "string" }, "storage_path": { "type": "string" }, "signed_url": { "type": "string", "description": "PUT the file's bytes here." }, "token": { "type": "string" }, "content_type": { "type": "string" } } }, "ListedFile": { "type": "object", "properties": { "name": { "type": "string" }, "storage_path": { "type": "string" }, "size": { "type": ["integer", "null"] }, "content_type": { "type": ["string", "null"] }, "uploaded_at": { "type": ["string", "null"], "format": "date-time" } } }, "RunAccepted": { "type": "object", "properties": { "run_id": { "type": "string", "format": "uuid" }, "status": { "type": "string", "example": "queued" }, "poll_url": { "type": "string", "format": "uri" } } }, "Question": { "type": "object", "properties": { "id": { "type": "string" }, "question": { "type": "string" }, "context": { "type": "string" }, "options": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "label": { "type": "string" } } } }, "allowCustom": { "type": "boolean" } } }, "Report": { "type": "object", "properties": { "inserted": { "type": "integer", "description": "Total rows imported across all sheets." }, "perSheet": { "type": "array", "items": { "type": "object", "properties": { "templateName": { "type": "string" }, "sheetId": { "type": "string" }, "rowCount": { "type": "integer" } } } }, "invalidCellCount": { "type": "integer", "description": "Rows that still carry at least one invalid cell." }, "coverage": { "type": ["object", "null"], "description": "Optional per-column fill/distinct profile of sources vs. outputs." } } }, "Run": { "type": "object", "description": "A run's polled state. Fields beyond run_id/status/created_at depend on status.", "properties": { "run_id": { "type": "string", "format": "uuid" }, "status": { "type": "string", "enum": ["queued", "running", "blocked", "awaiting_approval", "completed", "failed", "canceled"] }, "created_at": { "type": "string", "format": "date-time" }, "instructions": { "type": ["string", "null"], "description": "The guidance given when the migration was generated (at most 200 characters), carried onto every later leg. Null when none was given." }, "answers": { "type": ["object", "null"], "description": "A read-back of your last reply, present once you've answered a block." }, "message": { "type": ["string", "null"], "description": "Present while queued/running." }, "blocked_reason": { "type": "string", "enum": ["question", "credentials"], "description": "Present when blocked — which kind of block it is." }, "questions": { "type": "array", "items": { "$ref": "#/components/schemas/Question" }, "description": "Present when blocked for a question." }, "credential_request": { "$ref": "#/components/schemas/CredentialRequest" }, "report": { "$ref": "#/components/schemas/Report" }, "error": { "type": "string", "description": "Present when failed." } } }, "CredentialRequest": { "type": "object", "description": "Present when a run is blocked for credentials. Describes the live source needing secrets — render a form from `schema` and POST to .../credentials. Carries no secret values.", "properties": { "connection_id": { "type": "string", "format": "uuid", "description": "The source connection the secrets belong to." }, "source_key": { "type": "string" }, "source_name": { "type": "string" }, "reason": { "type": "string", "description": "Why the agent needs access. May be absent." }, "guidance": { "type": "object", "description": "Optional help — { summary?, steps?, links? } — for where to find the credentials." }, "schema": { "type": "object", "description": "The fields to collect, keyed by name.", "additionalProperties": true } } }, "SourceConnection": { "type": "object", "description": "A migration's live API source connection. Never includes secret values; `creds_configured` reports whether credentials are stored.", "properties": { "id": { "type": "string", "format": "uuid" }, "profile_id": { "type": ["string", "null"] }, "source_key": { "type": "string" }, "name": { "type": "string", "description": "Connector display name; falls back to source_key." }, "category": { "type": ["string", "null"] }, "config": { "type": "object", "description": "Non-secret connector configuration.", "additionalProperties": true }, "selected_streams": { "type": ["array", "null"], "items": { "type": "string" } }, "stream_state": { "type": "object", "additionalProperties": true }, "creds_configured": { "type": "boolean" }, "bound_source_extractor_id": { "type": ["string", "null"] }, "last_snapshot": { "type": ["object", "null"], "description": "Metadata about the most recent extraction." }, "last_sync_at": { "type": ["string", "null"], "format": "date-time" } } }, "Preview": { "type": "object", "properties": { "status": { "type": "string", "example": "awaiting_approval" }, "templates": { "type": "array", "items": { "type": "object", "properties": { "slug": { "type": "string" }, "name": { "type": "string" } } } }, "sheets": { "type": "array", "items": { "type": "object", "properties": { "template": { "type": "object", "properties": { "slug": { "type": "string" }, "name": { "type": "string" } } }, "rows": { "type": "array", "items": { "type": "array", "items": { "type": "string" } }, "description": "Rows as a grid, header row first." }, "invalid_cells": { "type": "array", "items": {} } } } } } }, "ThreadEvent": { "type": "object", "properties": { "sequence": { "type": "integer", "description": "Stable cross-run ordinal; use as the next `after`." }, "type": { "type": "string", "description": "message | thinking | status | tool_call | tool_result | task | request_input | prepared | preview | completed | error." }, "data": { "type": "object", "description": "The event payload, kept intact." }, "created_at": { "type": "string", "format": "date-time" } } }, "SessionLog": { "type": "object", "properties": { "migration_id": {"type":"string"}, "generated_at": {"type":"string"}, "status": {"$ref":"#/components/schemas/MigrationStatus"}, "covers": { "type": "object", "properties": { "runs": {"type":"integer"}, "from": {"type":"string","nullable":true}, "to": {"type":"string","nullable":true} } }, "runs": {"type":"array","items":{"$ref":"#/components/schemas/SessionLogRun"}} } }, "MigrationStatus": { "type": "string", "enum": ["awaiting_files","ready","awaiting_approval","completed"], "description": "- `awaiting_files` — no files uploaded and no source connection\n- `ready` — inputs present, no plan yet\n- `awaiting_approval` — a preview is waiting to be executed\n- `completed` — an import has run successfully\n" }, "MigrationKnowledge": { "type": "object", "properties": { "migration_id": {"type":"string"}, "knowledge": {"type":"string","nullable":true}, "updated_at": {"type":"string","nullable":true} } }, "KnowledgeWrite": { "type": "object", "required": ["knowledge"], "properties": { "knowledge": { "type": "string", "nullable": true, "description": "Replaces the whole document. Empty string clears it." }, "if_unmodified_since": { "type": "string", "description": "The `updated_at` you last read. Supply it to get `412` instead of silently overwriting a concurrent change." } } }, "ExportJob": { "type": "object", "properties": { "job_id": {"type":"string"}, "status": {"type":"string","enum":["pending","running","completed","failed"]}, "format": {"type":"string","enum":["csv","zip"]}, "rows_processed": {"type":"integer","nullable":true}, "total_rows": {"type":"integer","nullable":true}, "file_size": {"type":"integer","nullable":true}, "error": {"type":"string","nullable":true}, "expires_at": {"type":"string"}, "download_url": { "type": "string", "nullable": true, "description": "A time-limited link, present once the job is complete." } } }, "SourceKnowledge": { "type": "object", "properties": { "source": {"type":"string"}, "knowledge": {"type":"string","nullable":true}, "updated_at": {"type":"string","nullable":true} } }, "SessionLogRun": { "type": "object", "properties": { "run_id": {"type":"string"}, "kind": {"type":"string","description":"`generate`, `update`, `reuse`, `clarify` or `execute`."}, "status": {"type":"string"}, "started_at": {"type":"string","nullable":true}, "completed_at": {"type":"string","nullable":true}, "started_by": { "type": "string", "nullable": true, "description": "Null when the run was started through this API." }, "files": {"type":"array","items":{"type":"string"}}, "plan": {"type":"string","nullable":true}, "questions": {"type":"array","items":{"type":"object"}}, "answers": {"type":"object"}, "outcome": { "type": "object", "nullable": true, "properties": { "ok": {"type":"boolean","nullable":true}, "rows_written": {"type":"integer","nullable":true}, "per_sheet": { "type": "array", "items": { "type": "object", "properties": {"sheet":{"type":"string","nullable":true},"rows":{"type":"integer","nullable":true}} } }, "invalid_rows": {"type":"integer","nullable":true}, "error": {"type":"string","nullable":true}, "replaced_existing_data": { "type": "boolean", "description": "True when this run emptied its destination sheets before writing — it replaced the previous import rather than adding to it." } } }, "audit": { "type": "array", "description": "Who did what, in order.", "items": { "type": "object", "properties": { "at": {"type":"string"}, "event": {"type":"string","enum":["started","resumed","templates_adjusted","plan_approved","unknown"]}, "actor": {"type":"string","nullable":true}, "mode": {"type":"string"}, "answers_excerpt": {"type":"string"}, "changes": {"type":"array","items":{"type":"object"}}, "plan": {"type":"string"} } } } } } } } }