VRPlatformVRPlatform
Build a Product UI

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

ResourceLogical recordSupported CSV shape
Bank recordsOne CSV rowWide
OwnersOne owner, optionally linked to a listingWide
VendorsOne vendor per rowWide
ExpensesRows grouped by bill referenceWide or repeated lines
DepositsRows grouped by deposit referenceWide 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

ConcernUIAPI
File selection and progressRenderCreate upload and inspect bytes
Headers and delimiterDisplayDetect
Preset settingsRender from control metadataDefine and validate
Column mappingLet the user editSuggest, validate, and apply
Reference matchesLet the user choose stable IDsFind candidates and apply overrides
Preview totals and issuesDisplay and filterCompute
Large result setsPage on demandStore and serve paginated artifacts
Import eligibilityDisable or explainEnforce on confirmation
Record creationShow progress and outcomesProcess 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

StepRequestUI result
1Read resource and preset metadataRender supported fields and controls
2Create an uploadReceive uploadRef, uploadUrl, and expiry
3PUT the unchanged fileReceive headers, delimiter, and detection
4Create a previewReceive an operation acknowledgement
5Poll and page preview recordsReview valid, invalid, unresolved, and excluded records
6Create another preview after editsReview an immutable replacement plan
7Confirm the selected previewReceive an operation acknowledgement
8Poll and page import outcomesShow 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:

ControlExpected UI
accountOne account selector filtered by accountPurpose
accountMapSource-key to account selectors
stringText input
percentageNumeric input bounded by minimum and maximum
currencyMoneyMapCurrency-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 stateUI behavior
exactPreselect the candidate and ask the user to confirm its settings
possibleOffer the candidate alongside column mapping
ambiguousRequire the user to choose a candidate or column mapping
noneContinue 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:

StatusUI behavior
queued or runningContinue polling
completedRead the summary and records
failedMap 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 stateMeaningBlocks confirmation
validReady to importNo
invalidA value or relationship violates the contractYes
unresolvedA required reference needs a stable-ID choiceYes
excludedIntentionally outside the import planNo

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:

LayerReadUse
Requestcode, context.reason, and optional issuesChoose recovery and mark fields
Preview or importstatus: "failed" and failureExplain an asynchronous failure
Recordstate 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" }
}
ReasonCodeRecovery
uploadTooLarge, selectionTooLargePAYLOAD_TOO_LARGESelect a smaller file or mapping
uploadUnavailableNOT_FOUNDCreate and upload a new file reference
csvInvalidEncodingBAD_REQUESTExport the file as UTF-8
csvNoRowsBAD_REQUESTAdd data rows and upload the file again
csvBlankHeader, csvDuplicateHeadersBAD_REQUESTCorrect the headers and upload again
csvColumnLimitExceededBAD_REQUESTReduce the number of columns
csvRowLimitExceededBAD_REQUESTReduce the number of rows
presetConfigurationInvalidBAD_REQUESTMark fields from issues and keep the setup
presetSourceMismatchCONFLICTSelect a detected preset or use mapping
previewNotReadyCONFLICTContinue polling before confirmation
previewBlockingIssuesCONFLICTResolve or exclude blocking records
previewNoImportableRecordsCONFLICTInclude at least one valid record
previewStale, csvSourceChangedCONFLICTCreate a new preview from the current file
csvExactFileDuplicateCONFLICTReuse it or confirm with an intentional override
duplicateOverrideNotApplicableCONFLICTConfirm without the duplicate override
previewExpiredGONEStart a new workflow
importOutcomesExpiredGONEStop paging outcomes
previewRecordCursorInvalidBAD_REQUESTRestart preview paging
importOutcomeCursorInvalidBAD_REQUESTRestart 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 codeRecovery
csvPreviewTooLargeSplit the file and create a new preview
csvPreviewStorageFailedRetry a new preview from the uploaded file
csvImportPlanUnavailableCreate a new preview
csvImportPlanInvalidCreate a new preview
csvImportApplyFailedReview target records before retrying
csvImportOutcomeUnavailableReview target records; do not retry blindly
csvImportRecordFailedPage the failed outcome records
operationFailedReview 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 stateKeepPrimary action
File selectedLocal file and resourceStart upload
ConfiguringuploadRef, inspection, mapping or preset settingsPreview
PreviewingpreviewId and current setupPoll status
ReviewingSummary, current page, setupFix or confirm
ImportingimportIdPoll status
CompletedOutcome summary and filtersView records or finish
Preview failedUpload and setupCorrect and create another preview
Import status unknownimportIdRead 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

ResourceRules that affect the UI
Bank recordsSelect one account. Pre-start rows are locked exclusions and are not imported.
OwnersCreates owners and optional ownership from 1970-01-01 with no end date.
VendorsCreates active vendors, updates exact matches, and never creates ownership.
ExpensesRepeated bill references form one expense with multiple lines.
DepositsRepeated 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.

API Reference

On this page