Import Data from CSV
Build upload, mapping, preview, review, and confirmation workflows
Last Updated: 2026-09-12
Version: 1.14
CSV imports let a user review bank-record, owner, vendor, expense, or deposit files with different column layouts before any records are written. The API owns file inspection, preset detection, mapping contracts, validation, and processing. The UI owns the interaction around those API responses.
Use only the top-level /csv-imports routes for this workflow. The synchronous
Connection CSV endpoint is for existing fixed templates and has a separate
contract.
All requests use the normal API host and selected team:
x-api-key: <team-api-key>
x-team-id: <team-uuid>See Authentication for other supported credentials.
Supported Resources
| Resource | Logical record | Supported CSV shape |
|---|---|---|
| Bank records | One CSV row | Wide |
| Owners | One owner, optionally linked to a listing | Wide |
| Vendors | One vendor per row | Wide |
| Expenses | Rows grouped by bill reference | Wide or repeated lines |
| Deposits | Rows grouped by deposit reference | Wide or repeated lines |
Read GET /csv-imports/resources/{resource} before rendering mapping fields.
The response is the versioned contract for detection, mapping requirements,
row requirements, fixed values, and import effects. Do not copy these rules
into the client.
GET /csv-imports/resources/owners{
"resource": "owners",
"version": 1,
"label": "Owners",
"rowShapes": ["wide"],
"fields": [
{
"key": "ownerKind",
"label": "Owner kind",
"required": true,
"input": "column",
"detection": {
"aliases": ["Owner kind", "Kind", "Contact kind"],
"values": {
"individual": "individual",
"company": "company"
},
"deriveFrom": "companyType",
"derivedValues": {
"individual": "individual",
"c_corporation": "company"
}
}
},
{
"key": "companyType",
"label": "Company type",
"required": false,
"input": "column",
"detection": {
"aliases": ["Company type", "Business entity type", "Entity type"]
}
},
{
"key": "listing",
"label": "Listing",
"required": false,
"input": "column",
"detection": {
"aliases": ["Listing", "Listing ref", "Listing reference", "Property"]
}
}
],
"mappingRequirements": [
{ "type": "allOf", "fields": ["ownerKind"] }
],
"rowRequirements": [
{
"type": "when",
"field": "ownerKind",
"equals": "individual",
"require": "allOf",
"fields": ["firstName", "lastName"]
},
{
"type": "when",
"field": "ownerKind",
"equals": "company",
"require": "allOf",
"fields": ["companyName", "companyType"]
}
],
"fixedValues": [
{ "field": "type", "value": "owner" },
{ "field": "status", "value": "active" }
],
"effects": [
{
"when": { "field": "listing", "present": true },
"action": "createListingOwnership",
"startAt": "1970-01-01",
"endAt": null,
"businessModel": "managed",
"reserve": 0,
"split": "equal"
}
]
}The response orders fields for display. required is the compatibility view of
unconditional allOf mapping requirements. Use mappingRequirements and
rowRequirements for one-of and conditional rules. deriveFrom describes an
explicit detection rule; it is not a processor fallback. The example is
shortened. See the exact
resource metadata contract.
UI and API Responsibilities
| Concern | UI | API |
|---|---|---|
| File selection and progress | Render | Create upload and inspect bytes |
| Headers and delimiter | Display | Detect |
| Preset settings | Render from control metadata | Define and validate |
| Column mapping | Let the user edit | Suggest, validate, and apply |
| Reference matches | Let the user choose stable IDs | Find candidates and apply overrides |
| Preview totals and issues | Display and filter | Compute |
| Large result sets | Page on demand | Store and serve paginated artifacts |
| Import eligibility | Disable or explain | Enforce on confirmation |
| Record creation | Show progress and outcomes | Process idempotently |
Do not duplicate preset IDs, provider rules, or resource field catalogs in the client. Render the metadata returned by the API.
End-to-End Flow
| Step | Request | UI result |
|---|---|---|
| 1 | Read resource and preset metadata | Render supported fields and controls |
| 2 | Create an upload | Receive uploadRef, uploadUrl, and expiry |
| 3 | PUT the unchanged file | Receive headers, delimiter, and detection |
| 4 | Create a preview | Receive an operation acknowledgement |
| 5 | Poll and page preview records | Review valid, invalid, unresolved, and excluded records |
| 6 | Create another preview after edits | Review an immutable replacement plan |
| 7 | Confirm the selected preview | Receive an operation acknowledgement |
| 8 | Poll and page import outcomes | Show created, updated, existing, skipped, and failed records |
The API stores large files, preview plans, and import outcomes. The UI keeps only IDs, summaries, current form state, and the page being displayed.
1. Read Presets
Call GET /csv-imports/presets?resource=deposits when the user selects a
resource. A preset definition includes its configuration controls. The client
must not branch on values such as
stripe.payout-reconciliation.
{
"data": [
{
"id": "stripe.payout-reconciliation",
"version": 1,
"resource": "deposits",
"label": "Stripe payout reconciliation",
"description": "Import Stripe payout reconciliation exports as grouped deposits.",
"variants": ["Stripe payout reconciliation report"],
"settings": [
{
"key": "bankAccountId",
"label": "Bank account",
"description": "Account that received the Stripe payout.",
"control": "account",
"required": true,
"accountPurpose": "bank"
}
]
}
]
}The Stripe preset exposes only the payout bank account. Its category and fee
lines use the configured transaction line mappings. Render every returned setting
by its control discriminator:
| Control | Expected UI |
|---|---|
account | One account selector filtered by accountPurpose |
accountMap | Source-key to account selectors |
string | Text input |
percentage | Numeric input bounded by minimum and maximum |
currencyMoneyMap | Currency-key to money inputs |
Use inspection candidate suggestions to seed map keys or values. A preset
version is part of its identity. If the selected candidate ID or version
changes, discard the previous candidate's setting values and initialize the
new controls again.
See the exact preset metadata contract.
2. Upload and Inspect
Create the upload descriptor first:
POST /csv-imports/uploads
Content-Type: application/json
{
"fileName": "stripe-payout.csv",
"fileSize": 48210,
"resource": "deposits"
}{
"expiresAt": "2026-08-24T14:30:00.000+00:00",
"uploadRef": "upload-ref",
"uploadUrl": "https://api.vrplatform.app/csv-imports/uploads/upload-ref"
}Send the file bytes unchanged to the returned URL. Do not JSON-encode the file or parse it in the browser.
PUT <uploadUrl>
Content-Type: text/csv
<raw CSV bytes>{
"status": "uploaded",
"inspection": {
"encoding": "utf-8",
"delimiter": ",",
"headerRow": 1,
"headers": [
"automatic_payout_id",
"automatic_payout_effective_at",
"balance_transaction_id",
"reporting_category",
"gross",
"fee",
"net",
"currency",
"description"
],
"detection": {
"state": "exact",
"candidates": [
{
"id": "stripe.payout-reconciliation",
"version": 1,
"resource": "deposits",
"label": "Stripe payout reconciliation",
"suggestions": {},
"evidence": {
"forbiddenHeaders": [],
"matchedHeaders": [
"automatic_payout_id",
"automatic_payout_effective_at",
"balance_transaction_id",
"reporting_category",
"gross",
"fee",
"net",
"currency",
"description"
],
"missingHeaders": []
}
}
]
}
}
}Detection guides the choice; it does not import anything.
| Detection state | UI behavior |
|---|---|
exact | Preselect the candidate and ask the user to confirm its settings |
possible | Offer the candidate alongside column mapping |
ambiguous | Require the user to choose a candidate or column mapping |
none | Continue with column mapping |
Files must be UTF-8 and may contain at most 100,000,000 bytes, 100 columns, and 1,000,000 data rows. A physical CSV record may contain at most 256 KiB. Grouped owner records may contain 100 rows; other grouped records may contain 1,000 rows. Uploads expire after 24 hours.
The upload reference retains the selected resource. Do not resend it when uploading bytes, inspecting the upload, or creating a preview.
GET /csv-imports/uploads/{uploadRef} repeats inspection while the upload
remains available.
See create upload, upload bytes, and inspect upload.
3. Choose a Preview Selection
Every preview uses one of three selection contracts.
Automatic mapping
Use automatic mapping as the first mapping attempt. Bank records additionally require the selected bank account.
{
"uploadRef": "upload-ref",
"selection": {
"type": "detection"
}
}When completed, result.details.mapping contains the API's mapping proposal.
Render it in the mapping form. Submit an edited mapping as a new preview.
Preset
Send the selected candidate's exact ID and version plus values for the settings returned by preset metadata.
{
"uploadRef": "upload-ref",
"selection": {
"type": "preset",
"id": "stripe.payout-reconciliation",
"version": 1,
"configuration": {
"bankAccountId": "11111111-1111-4111-8111-111111111111"
}
}
}The API reads fees from the export. It does not ask the UI to reconstruct or split Stripe fees.
Explicit mapping
Mapping contracts differ by resource. This bank-record example maps one signed amount column:
{
"uploadRef": "upload-ref",
"selection": {
"type": "mapping",
"accountId": "11111111-1111-4111-8111-111111111111",
"mapping": {
"resource": "bankRecords",
"version": 1,
"delimiter": ",",
"headerRow": 1,
"excludedRows": [],
"reference": {
"source": "Transaction ID"
},
"date": {
"source": "Transaction date",
"format": "yyyy-mm-dd"
},
"description": {
"sources": ["Description"],
"operation": "join"
},
"amount": {
"mode": "signed",
"source": "Amount",
"debitSign": -1,
"creditSign": 1
}
}
}
}mapping.reference is optional for bank records. Map it when the export provides
a stable transaction identifier. The identifier becomes the bank record's exact
CSV identity within the selected account. Rows without a mapped or populated reference
receive a deterministic CSV identity. A populated reference may appear only once in a file;
later rows with the same reference are invalid.
For owner, vendor, expense, and deposit mappings, each field carries its source column and any value or stable-ID overrides. Use the generated create preview contract for the exact resource-specific schemas.
Changing a mapping, excluded row, preset configuration, or reference override
does not mutate an existing preview. Post the same uploadRef with the new
selection and review the new preview ID.
Date formats
Set mapping.date.format for bank records, or the date field's format in
expense/deposit mappings, to select yyyy-mm-dd, mm/dd/yyyy, dd/mm/yyyy,
or dd.mm.yyyy. When a mapped date column has no explicit format, the API
streams the full file and considers only values that are valid calendar dates
in at least one supported format. Explicitly excluded rows contribute no evidence.
Each date column is inferred independently; there is no locale default.
Ambiguous slash dates such as 1/2/2027 require a format choice unless another
valid date in that column identifies the order. Conflicting evidence anywhere
in the file, such as 01/23/2026 together with 23/01/2026, leaves the format
unset. Invalid values such as 32/01/2026 cannot select a format and still fail
row validation. Correct the file or choose one format and exclude incompatible
rows. Every row is validated against the selected format; impossible dates never
roll into another month or year. An explicit format always takes precedence.
Header and delimiter detection still samples the first 64 KiB and up to 20 lines. Date inference requires an additional streaming pass only when a mapped date column lacks a format. No rows are accepted before that pass completes.
4. Poll and Review the Preview
Creating a preview returns immediately:
{
"operationId": "33333333-3333-4333-8333-333333333333",
"type": "csv-import-preview",
"status": "queued",
"resource": null
}Use operationId as the preview ID. Poll
GET /csv-imports/previews/{id} while the status is queued or running.
A completed response contains a bounded summary, not every record:
{
"id": "33333333-3333-4333-8333-333333333333",
"status": "completed",
"failure": null,
"result": {
"logicalRecordCount": 2450,
"stateCounts": {
"valid": 2431,
"invalid": 3,
"unresolved": 12,
"excluded": 4
},
"issueCounts": {
"listingNotFound": 12,
"invalidAmount": 3
},
"fileHash": "c6b06de0c87b0e3f899aec6147aa8d32dc764fda90ec9767dd90ea9657e381aa",
"planHash": "f88b4f13a15a3cfc7b72c80ec52dfa6a7fe12bcc640eec93c4ac74239d25f28f"
}
}Operation status uses the shared lifecycle vocabulary:
| Status | UI behavior |
|---|---|
queued or running | Continue polling |
completed | Read the summary and records |
failed | Map failure.code to UI copy and allow recovery |
GET /operations/{id} returns the same status and failure. The CSV status
endpoint adds the completed preview result used by this workflow.
Use the counts for tabs and badges. Fetch table rows from the records endpoint:
The records query uses state for validation state. status is reserved for
the preview or import lifecycle.
GET /csv-imports/previews/33333333-3333-4333-8333-333333333333/records?state=unresolved&limit=100{
"data": [
{
"resource": "owners",
"recordRef": "row-14",
"rowNumbers": [14],
"state": "unresolved",
"source": [
{
"Listing": "Beach House",
"Owner email": "owner@example.com"
}
],
"resolutions": [
{
"field": "listing",
"sourceValue": "Beach House",
"state": "ambiguous",
"candidates": [
{
"id": "44444444-4444-4444-8444-444444444444",
"label": "Beach House",
"secondaryLabel": "Miami, FL"
}
],
"candidateCount": 2,
"candidatesTruncated": true
}
],
"excluded": false,
"issues": [
{
"code": "referenceUnresolved",
"field": "listing",
"rowNumber": 14,
"message": "Listing reference is unresolved"
}
]
}
],
"nextCursor": "<opaque-cursor>"
}Pass nextCursor unchanged to fetch the next page. The maximum page size is
250. A cursor belongs to its preview, state filter, and page size. Do not build
cursors or keep all preview records in client memory.
To resolve the example, add an override from Beach House to the selected
candidate UUID in the mapping and create a new preview. Candidate labels are
for display; send the stable candidate ID.
| Record state | Meaning | Blocks confirmation |
|---|---|---|
valid | Ready to import | No |
invalid | A value or relationship violates the contract | Yes |
unresolved | A required reference needs a stable-ID choice | Yes |
excluded | Intentionally outside the import plan | No |
Confirmation also requires at least one valid record. See preview status and preview records.
Highlight invalid cells
Field-specific issues carry the canonical mapping key in field, such as
date, amount, or appliedAmount. details.sourceColumns identifies the
original CSV headers when the field has a source column. Whole-record issues
may omit field. Expense mappingRequired issues identify each missing field.
When a mapped expense date needs an explicit format, preview-record issues use
field for the date mapping and details.sourceColumns for its CSV header. For
example:
{
"code": "invalidAmount",
"field": "amount",
"rowNumber": 3,
"message": "Amount must be a number",
"details": { "sourceColumns": ["Withdrawal"] }
}Inspect possible duplicate bank records
Bank previews compare account, date, and signed amount with active existing
CSV and bank-feed transactions and earlier eligible rows in the upload. A value
match is a possible duplicate; it never merges, discards, or blocks a transaction.
Exact identity matches update the existing record. Map mapping.reference when
the source provides a stable transaction identifier. Generated identities include
the logical row position, so reordered exports can still produce value warnings.
A possible match adds issues[].code: "possibleDuplicate" and a typed
duplicateReview object with matching record IDs or upload row numbers, display
values, and candidate counts. Candidate lists contain at most 20 entries;
candidateCount and candidatesTruncated describe larger sets. Candidate
descriptions are display excerpts of at most 255 characters. The row remains valid
and is included unless its row number is added to mapping.excludedRows.
result.details.duplicateCounts.possible reports the number of warnings.
Use issueCode=possibleDuplicate on preview and import records to page these
rows, optionally with a state filter. Cursors belong to both filters.
Project the bank ending balance
Bank previews return result.details.balanceProjection:
{
"from": "2026-01-01",
"asOf": "2026-01-31",
"currentEndingBalance": 100000,
"projectedEndingBalance": 125000,
"netChange": 25000,
"hasBalanceAnchor": true
}Amounts are cents in the selected bank account's currency. Balances are
projected through the end of the asOf day.
from and asOf are the earliest and latest dates among rows that pass the
file checks: a valid date and amount, and no repeated reference. Rows in
mapping.excludedRows and pre-start rows never set them.
Use them to display the statement period the upload covers.
These dates are read before rows are checked against the bank account and
existing records. A row that the later check marks invalid still sets from
or asOf. A row with only a possibleDuplicate warning stays valid and
counts toward both the period and the amounts. currentEndingBalance,
projectedEndingBalance, netChange, and totals use only rows that end
valid, so a period date can belong to a row that adds no amounts, and the
period can be wider than the rows that will be imported. Both dates are null
when no row qualifies. Previews created before from existed omit it.
Identity updates replace existing activity rather than adding it twice.
The projection uses the committed bank-balance calculation, including balance
snapshots, corrections, and historical balances inferred from a later snapshot.
It can differ from current balance plus CSV net. It does not create ledger
transactions or infer missing movements. Summary rows must be excluded from a
transaction CSV. With no recorded balance anchor, hasBalanceAnchor is false
and the existing zero-based bank calculation applies. Label the display
Projected ending balance based on available records.
The projection is a preview snapshot. If bank records change before confirmation,
confirmation returns previewStale; build and review a new preview. Repeated
confirmation of an already-confirmed preview still returns its existing import.
Changes after confirmation can change the eventual balance. Read the account
again after import completes.
5. Confirm and Read Outcomes
Confirm the reviewed preview without resending its mapping or file:
POST /csv-imports/previews/33333333-3333-4333-8333-333333333333/confirm
Content-Type: application/json{}{
"operationId": "55555555-5555-4555-8555-555555555555",
"type": "csv-import",
"status": "queued",
"resource": null
}Use operationId as the import ID. Repeated confirmation of the same preview
returns the same import. Poll GET /csv-imports/{id} until it reaches a final
status.
Confirming another preview for the same exact file and target returns a conflict after a completed import. To intentionally import it again, send:
{
"duplicateOverride": {
"reason": "intentionalReimport"
}
}If the prior exact attempt is queued, started, failed, or canceled, confirmation
returns that attempt. Send the override only when another attempt is intended.
If no exact prior import exists, the override returns 409 CONFLICT with
context.reason: duplicateOverrideNotApplicable; confirm without the override.
Exact-file identity uses the raw file hash and target scope. For bank records, the target is the selected bank account. Similar normalized rows remain non-blocking warnings and are never merged automatically.
{
"id": "55555555-5555-4555-8555-555555555555",
"status": "completed",
"failure": null,
"outcome": {
"created": 2410,
"updated": 18,
"existing": 3,
"skipped": 0,
"failed": 0
}
}Page detailed outcomes when the user needs record-level results:
GET /csv-imports/55555555-5555-4555-8555-555555555555/records?state=created&limit=100{
"data": [
{
"recordRef": "row-14",
"rowNumbers": [14],
"state": "created",
"entityIds": ["66666666-6666-4666-8666-666666666666"],
"issues": []
}
],
"nextCursor": null
}See confirm preview, import status, and import records.
Failure Contract
A bank-record duplicate warning returned during the write does not turn a
successfully written row into a failed outcome. Read state, entityIds, and
issues together: a created or updated record can carry possibleDuplicate,
including when another feed changed after preview. Review those records before
retrying an import.
CSV failures use three complementary contracts:
| Layer | Read | Use |
|---|---|---|
| Request | code, context.reason, and optional issues | Choose recovery and mark fields |
| Preview or import | status: "failed" and failure | Explain an asynchronous failure |
| Record | state and issues[] | Fix, resolve, or exclude one record |
Request failures use the standard error contract.
Branch on code and context.reason; never parse message.
{
"code": "BAD_REQUEST",
"message": "CSV preset configuration is invalid",
"issues": [
{
"message": "Invalid UUID",
"path": ["selection", "configuration", "bankAccountId"]
}
],
"context": { "reason": "presetConfigurationInvalid" }
}| Reason | Code | Recovery |
|---|---|---|
uploadTooLarge, selectionTooLarge | PAYLOAD_TOO_LARGE | Select a smaller file or mapping |
uploadUnavailable | NOT_FOUND | Create and upload a new file reference |
csvInvalidEncoding | BAD_REQUEST | Export the file as UTF-8 |
csvNoRows | BAD_REQUEST | Add data rows and upload the file again |
csvBlankHeader, csvDuplicateHeaders | BAD_REQUEST | Correct the headers and upload again |
csvColumnLimitExceeded | BAD_REQUEST | Reduce the number of columns |
csvRowLimitExceeded | BAD_REQUEST | Reduce the number of rows |
presetConfigurationInvalid | BAD_REQUEST | Mark fields from issues and keep the setup |
presetSourceMismatch | CONFLICT | Select a detected preset or use mapping |
previewNotReady | CONFLICT | Continue polling before confirmation |
previewBlockingIssues | CONFLICT | Resolve or exclude blocking records |
previewNoImportableRecords | CONFLICT | Include at least one valid record |
previewStale, csvSourceChanged | CONFLICT | Create a new preview from the current file |
csvExactFileDuplicate | CONFLICT | Reuse it or confirm with an intentional override |
duplicateOverrideNotApplicable | CONFLICT | Confirm without the duplicate override |
previewExpired | GONE | Start a new workflow |
importOutcomesExpired | GONE | Stop paging outcomes |
previewRecordCursorInvalid | BAD_REQUEST | Restart preview paging |
importOutcomeCursorInvalid | BAD_REQUEST | Restart outcome paging |
The exact reason union for each request is included in its OpenAPI response.
Asynchronous preview source failures use csvFileTooLarge,
csvColumnLimitExceeded, csvRowLimitExceeded, csvLogicalRecordTooLarge,
csvInvalidEncoding, csvNoRows, csvParsingFailed, csvSourceChanged, or
csvSourceUnavailable.
| Asynchronous code | Recovery |
|---|---|
csvPreviewTooLarge | Split the file and create a new preview |
csvPreviewStorageFailed | Retry a new preview from the uploaded file |
csvImportPlanUnavailable | Create a new preview |
csvImportPlanInvalid | Create a new preview |
csvImportApplyFailed | Review target records before retrying |
csvImportOutcomeUnavailable | Review target records; do not retry blindly |
csvImportRecordFailed | Page the failed outcome records |
operationFailed | Review target records and contact support if repeated |
The import may have written records when its code is csvImportApplyFailed,
csvImportOutcomeUnavailable, or operationFailed. Do not automatically retry
those operations. The API validates the complete reviewed plan before its first
resource write. An internally canceled job is exposed through this operation
surface as status: "failed" with failure.code: "operationFailed".
{
"id": "33333333-3333-4333-8333-333333333333",
"status": "failed",
"failure": {
"code": "csvInvalidEncoding",
"message": "CSV must use UTF-8 encoding"
}
}Keep the upload and selection after a preview failure so the user can correct
the problem. Keep the import ID after an import failure so its final status is
not lost. Request import records only when the status response includes
outcome; a terminal status without totals has no readable outcome manifest.
UI State and Recovery
Keep the workflow explicit. A modal with one undifferentiated loading state is not enough for a durable asynchronous import.
| UI state | Keep | Primary action |
|---|---|---|
| File selected | Local file and resource | Start upload |
| Configuring | uploadRef, inspection, mapping or preset settings | Preview |
| Previewing | previewId and current setup | Poll status |
| Reviewing | Summary, current page, setup | Fix or confirm |
| Importing | importId | Poll status |
| Completed | Outcome summary and filters | View records or finish |
| Preview failed | Upload and setup | Correct and create another preview |
| Import status unknown | importId | Read status before any retry |
Apply these client rules:
- Replacing the file clears its
uploadRef, candidate, settings, mapping, preview, and import state. - A preview error must not discard the uploaded file or the user's setup.
- Changing a preset candidate or version reinitializes its settings.
- Disable repeated upload and confirmation submissions while their request is active.
- Prevent accidental modal closure while file upload or confirmation is in flight, or require explicit confirmation before abandoning it.
- If confirmation loses its response, read the known preview or import status. Do not create a second workflow automatically.
- Page preview and outcome records. Do not download the complete result set into the browser.
- Render API issue messages and fields beside the affected record. Use issue codes for UI behavior, not for user-facing copy.
Resource Rules
| Resource | Rules that affect the UI |
|---|---|
| Bank records | Select one account. Pre-start rows are locked exclusions and are not imported. |
| Owners | Creates owners and optional ownership from 1970-01-01 with no end date. |
| Vendors | Creates active vendors, updates exact matches, and never creates ownership. |
| Expenses | Repeated bill references form one expense with multiple lines. |
| Deposits | Repeated references form one deposit with multiple lines. |
The CSV preview behavior differs from direct or feed ingestion. A reviewed pre-start CSV row stays in the preview as a system exclusion and creates no BankRecord. A pre-start row received through an AccountConnection or direct bank-record batch is stored as inactive history. Neither path changes opening balance or active bank totals.
An owner row without a listing creates or updates only the owner contact. A row
with a listing also creates ownership from 1970-01-01. Owner kind can come from
an explicit individual/company mapping or from Company Type: individual
selects an individual, while a mapped entity type selects a company. The owners
resource always writes an owner contact, regardless of a source column named
Type.
A generic Type header is not detected as owner kind or vendor kind. Use an
explicit kind column, a constant, or the documented owner derivation. Automatic
expense and deposit mapping follows the full-file date rules
for each mapped date column. The API does not assume ISO format.
Owner imports do not configure ownership percentages, dates, reserves, or business models. Existing unrelated ownership can block the affected listing. Name-only owner and vendor matches remain suggestions until reviewed. Blank CSV contact values do not clear existing contact fields.
Expenses and deposits accept either amounts on one wide row or repeated line rows under the same reference. The API normalizes both forms into one logical transaction before previewing it.
Fixed-Template Boundary
POST /connections/{id}/csv-import remains the synchronous endpoint for
existing fixed templates. It accepts its documented fixed-template inputs and
does not accept upload, mapping, preview, confirmation, status, or
reviewed-record fields.
New CSV import UI work must use /csv-imports. Do not add compatibility branches
to the Connection endpoint.
