Imports
Import customers, vendors, accounts, opening balances and open items from CSV or Excel: dry run, all-or-nothing commit, templates.
| Operation | Summary | Scope | Idempotency-Key |
|---|---|---|---|
POST /v1/entities/{entity}/imports |
Import customers, vendors, accounts, balances or open items | draft |
required |
POST /v1/entities/{entity}/imports/salesforce |
Import a Salesforce export | draft |
required |
GET /v1/entities/{entity}/imports/templates/{type} |
Download an import template | read |
POST /v1/entities/{entity}/imports
Import customers, vendors, accounts, balances or open items
Bring a customer's existing books into the ledger from a CSV or Excel file.
Start with a dry run (mode=dry_run, the default): it runs the same code as the
import in a savepoint, rolls it back, and returns every problem of the file at once, each
with its line (row of the file), column and message, plus a summary of what a commit
would create. Fix the file, repeat, then send it again with mode=commit and an
Idempotency-Key. A commit is all or nothing: it applies every row in one
transaction, or, if any row has a problem, answers 422 with the same list of problems and
changes nothing. A retry with the same key and file returns the first answer.
The six types go through the same services as the API does, so every control applies:
customers,vendors: created likePOST /v1/entities/{entity}/parties. A customer is active; a person's new vendor is a draft with an activation request, and a second person must approve it (GET /v1/approvals) before its bills can be posted or paid, unless the organization's approval policy allowsactivate_partyfor vendors.accounts: added likePOST /v1/entities/{entity}/accounts(needs the admin scope and the policy'smanage_accounts).opening_balances: the trial balance of the old books, one balanced opening entry per entity datedas_of(sourceopening), posted through the approval policy. Rows for the receivables and payables control accounts are not posted (their balances are the open items); the difference is booked to the account with the roleopening_balance_clearing. A second opening entry is refused until the first is reversed: to correct an opening entry, reverse it (POST /v1/journal-entries/{id}/reverse; a second person approves the reversal), then import the balances again.open_receivables,open_payables: every open invoice, bill or credit note becomes a posted document with its original number, dates and open gross amount, booked against the clearing account (receivables or payables control account against clearing), so nothing is counted twice. The customer or vendor must exist and be active. Once the balances and both lists agree, the clearing account is zero (summary.clearing_balance).
An fx_rate more than 10% away from the reference rate (ECB) of the opening date is a
problem of its row unless confirm_rates is true (see confirm_rates).
Agent keys are refused (403). Refused: 404 for an unknown entity, 422 for an unreadable
file, a file over 5 MB or 2,000 rows, a missing as_of, and (on commit) any problem in the
rows; 409 when the Idempotency-Key was used with another file. The commit is an audit
event (import.commit) with the file's SHA-256 next to the audit events of everything it
created.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | import_file_v1_entities__entity__imports_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entity |
string | yes | Entity code, e.g. CH01 (GET /v1/entities). |
Request body
multipart/form-data (required): Body_import_file_v1_entities__entity__imports_post
| Field | Type | Required | Description |
|---|---|---|---|
file |
string | yes | The file (multipart form field file): CSV (UTF-8; ,, ; or tab delimited) or Excel .xlsx, whose first row names the columns of the template (GET /v1/entities/{entity}/imports/templates/{type}); at most 5 MB and 2,000 rows. |
type |
"accounts" | "customers" | "open_payables" | "open_receivables" | "opening_balances" | "vendors" |
yes | What the rows are: customers, vendors, accounts, opening_balances (the trial balance of the old books), open_receivables or open_payables (the invoices and bills still open). |
mode |
"commit" | "dry_run" |
dry_run (the default) checks everything and saves nothing; commit applies the whole file or nothing and needs an Idempotency-Key. (default "dry_run") |
|
as_of |
string (date) or null | The opening date, YYYY-MM-DD: the day the new books start and the opening entries are dated (the balances are those at the end of the day before). Required for opening_balances, open_receivables and open_payables, whose period must be open; ignored by the other types. |
|
confirm_rates |
boolean | Use the fx_rate values of the file even where one differs by more than 10% from the reference rate (ECB) of the opening date. Without it such a row is a problem (rates_to_confirm counts them); with it the row is imported and listed as a warning. (default false) |
Example request
curl -X POST "$ERP_URL/v1/entities/CH01/imports" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F "file=@customers.csv" \
-F "type=accounts"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | DataImportResult |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"type": "accounts",
"mode": "commit",
"entity": "string",
"format": "csv",
"filename": "string",
"sha256": "string",
"as_of": "2026-09-21",
"rows": 1,
"ok": true,
"committed": true,
"errors": [
{
"line": 1,
"column": "string",
"message": "string"
}
],
"warnings": [
{
"line": 1,
"column": "string",
"message": "string"
}
],
"summary": {
"created": 1,
"waiting_for_activation": 1,
"posted": 1,
"waiting_for_approval": 1,
"total_debit": "string",
"total_credit": "string",
"clearing_difference": "string",
"amounts_by_currency": {},
"clearing_balance": "string",
"entry": {
"entry_id": null,
"status": null,
"number": null,
"approval_id": null
}
},
"rates_to_confirm": 0
}
POST /v1/entities/{entity}/imports/salesforce
Import a Salesforce export
Bring a company's Salesforce data over, one object (one file) at a time: its users (linked to the people here by email), accounts (customers), products, price books and prices, opportunities (deals), and the tasks, events and notes about them.
Import the files in the order of object: each links to those before it (a deal to its
account, a task to its deal, a price to its product and price book). Start with a dry
run (the default): it returns every problem with its line and the file's column, the
mapping of our fields to the file's columns (by Salesforce API name or report label), and
a summary of what a commit would create. Name other columns in mapping where the file
calls a field differently (a custom field), then send the file with mode=commit and an
Idempotency-Key. A commit is all or nothing.
Re-imports create nothing twice: every record is kept with its Salesforce id, so the
same file or a newer export imports only what is new (summary.already_imported counts the
rest). An account whose code or name is already a customer here is linked to it, not
doubled. Rows deleted in Salesforce (IsDeleted) are skipped.
Every record goes through the same controls as the API: customers under the approval
policy, VAT numbers checked (one that fails is kept and marked unverified), products on the
default revenue account. Agent keys are refused (403). Refused: 404 for an unknown entity,
422 for an unreadable file, a file over the limits, a bad mapping, and (on commit) any
problem in the rows; 409 when the Idempotency-Key was used with another file. The commit
is an audit event (import.salesforce) with the file's SHA-256.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | import_salesforce_v1_entities__entity__imports_salesforce_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entity |
string | yes | Entity code, e.g. CH01 (GET /v1/entities). |
Request body
multipart/form-data (required): Body_import_salesforce_v1_entities__entity__imports_salesforce_post
| Field | Type | Required | Description |
|---|---|---|---|
file |
string | yes | The export (multipart form field file): a CSV as Salesforce writes it (the weekly Data Export, Data Loader, Workbench or a report) or an Excel .xlsx; at most 20 MB, 10,000 rows and 800 columns. |
object |
"accounts" | "contacts" | "events" | "notes" | "opportunities" | "price_book_entries" | "price_books" | "products" | "tasks" | "users" |
yes | What the file holds, imported in this order: users (User), accounts (Account), products (Product2), price_books (Pricebook2), price_book_entries (PricebookEntry), opportunities (Opportunity), tasks (Task), events (Event), notes (Note). |
mode |
"commit" | "dry_run" |
dry_run (the default) checks everything and saves nothing; commit applies the whole file or nothing and needs an Idempotency-Key. (default "dry_run") |
|
mapping |
string or null | Optional JSON object: our field -> the file's column to read it from, or null to leave it empty. Fields it does not name are found by their Salesforce names (the mapping of a dry run shows what was found). |
Example request
curl -X POST "$ERP_URL/v1/entities/CH01/imports/salesforce" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F "file=@statement.xml" \
-F "object=accounts"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | SalesforceImportResult |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"object": "accounts",
"mode": "commit",
"entity": "string",
"format": "csv",
"filename": "string",
"sha256": "string",
"rows": 1,
"ok": true,
"committed": true,
"columns": [
"string"
],
"mapping": [
{
"field": "string",
"column": "string",
"required": true,
"description": "string",
"label": "string"
}
],
"unused_columns": [
"string"
],
"errors": [
{
"line": 1,
"column": "string",
"message": "string"
}
],
"warnings": [
{
"line": 1,
"column": "string",
"message": "string"
}
],
"summary": {
"created": 1,
"already_imported": 1,
"skipped": 1,
"records": {}
}
}
GET /v1/entities/{entity}/imports/templates/{type}
Download an import template
The columns of an import as a file you can fill in: csv or xlsx. The first row names
the columns (the onboarding workflow lists them);
amounts are plain decimals with a dot, dates YYYY-MM-DD, yes/no columns take yes or
no. Every cell of the template is text a spreadsheet cannot run as a formula. The
entity must be the caller's (404 otherwise).
| Required scope | read (admin implies every scope) |
| Operation id | download_template_v1_entities__entity__imports_templates__type__get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entity |
string | yes | Entity code, e.g. CH01 (GET /v1/entities). |
| path | type |
"accounts" | "customers" | "open_payables" | "open_receivables" | "opening_balances" | "vendors" |
yes | The kind of import: customers, vendors, accounts, opening_balances, open_receivables or open_payables. |
| query | format |
"csv" | "xlsx" |
csv (UTF-8 with a byte order mark, comma delimited) or xlsx. (default "csv") |
|
| query | examples |
boolean | Include two example rows (default); false gives the header row only. Delete the examples before importing: they are rows like any other. (default true) |
Example request
curl "$ERP_URL/v1/entities/CH01/imports/templates/vendors" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | The template file: a header row with the columns, and two example rows unless examples=false. |
text/csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Schemas
Body_import_file_v1_entities__entity__imports_post
| Field | Type | Required | Description |
|---|---|---|---|
file |
string | yes | The file (multipart form field file): CSV (UTF-8; ,, ; or tab delimited) or Excel .xlsx, whose first row names the columns of the template (GET /v1/entities/{entity}/imports/templates/{type}); at most 5 MB and 2,000 rows. |
type |
"accounts" | "customers" | "open_payables" | "open_receivables" | "opening_balances" | "vendors" |
yes | What the rows are: customers, vendors, accounts, opening_balances (the trial balance of the old books), open_receivables or open_payables (the invoices and bills still open). |
mode |
"commit" | "dry_run" |
dry_run (the default) checks everything and saves nothing; commit applies the whole file or nothing and needs an Idempotency-Key. (default "dry_run") |
|
as_of |
string (date) or null | The opening date, YYYY-MM-DD: the day the new books start and the opening entries are dated (the balances are those at the end of the day before). Required for opening_balances, open_receivables and open_payables, whose period must be open; ignored by the other types. |
|
confirm_rates |
boolean | Use the fx_rate values of the file even where one differs by more than 10% from the reference rate (ECB) of the opening date. Without it such a row is a problem (rates_to_confirm counts them); with it the row is imported and listed as a warning. (default false) |
Body_import_salesforce_v1_entities__entity__imports_salesforce_post
| Field | Type | Required | Description |
|---|---|---|---|
file |
string | yes | The export (multipart form field file): a CSV as Salesforce writes it (the weekly Data Export, Data Loader, Workbench or a report) or an Excel .xlsx; at most 20 MB, 10,000 rows and 800 columns. |
object |
"accounts" | "contacts" | "events" | "notes" | "opportunities" | "price_book_entries" | "price_books" | "products" | "tasks" | "users" |
yes | What the file holds, imported in this order: users (User), accounts (Account), products (Product2), price_books (Pricebook2), price_book_entries (PricebookEntry), opportunities (Opportunity), tasks (Task), events (Event), notes (Note). |
mode |
"commit" | "dry_run" |
dry_run (the default) checks everything and saves nothing; commit applies the whole file or nothing and needs an Idempotency-Key. (default "dry_run") |
|
mapping |
string or null | Optional JSON object: our field -> the file's column to read it from, or null to leave it empty. Fields it does not name are found by their Salesforce names (the mapping of a dry run shows what was found). |
DataImportResult
| Field | Type | Required | Description |
|---|---|---|---|
type |
"accounts" | "customers" | "open_payables" | "open_receivables" | "opening_balances" | "vendors" |
yes | The kind of import. |
mode |
"commit" | "dry_run" |
yes | dry_run (nothing is saved) or commit (all or nothing). |
entity |
string | yes | Code of the entity the import is for. |
format |
"csv" | "xlsx" |
yes | csv or xlsx, as detected from the file. |
filename |
string | yes | The uploaded file's name. |
sha256 |
string | yes | SHA-256 of the file, kept in the audit trail. |
as_of |
string (date) or null | yes | The opening date of an opening_balances or open item import; null otherwise. |
rows |
integer | yes | Data rows in the file (empty rows are skipped). |
ok |
boolean | yes | True when the file has no error: a dry run says a commit would succeed; a commit has succeeded. |
committed |
boolean | yes | True only for a commit that changed the books; always false for a dry run. |
errors |
array of ImportIssue |
yes | Every row-level problem found (all of them, not the first): a dry run lists them with 200, a commit refuses with 422 and the same list. |
warnings |
array of ImportIssue |
yes | Things worth knowing that do not stop the import, e.g. an ignored column or a trial balance row for a control account. |
summary |
ImportSummary |
yes | What was (or would be) created. |
rates_to_confirm |
integer | Rows whose fx_rate differs by more than 10% from the reference rate of the opening date: problems unless the import was sent with confirm_rates, warnings with it. (default 0) |
FieldMapping
Which column of the file one of our fields is read from.
| Field | Type | Required | Description |
|---|---|---|---|
field |
string | yes | Our field, e.g. name or country. |
column |
string or null | yes | The file's column it is read from (as the header names it, compared without case, spaces and hyphens); null when the file has none and the field is left empty. |
required |
boolean | yes | True when a row cannot be imported without it. |
description |
string | yes | What the field is, and the Salesforce field it is. |
label |
string or null | The name of a field of the organization's own (custom.<key>); null for the import's fields. |
ImportEntry
The opening entry an opening_balances import posted or queued.
| Field | Type | Required | Description |
|---|---|---|---|
entry_id |
string (uuid) | yes | Id (uuid) of the journal entry. |
status |
string | yes | posted, or pending_approval when the approval policy sends it to a second person. |
number |
string or null | yes | The entry number once posted; null before. |
approval_id |
string (uuid) or null | yes | The approval request (GET /v1/approvals) while it waits; null otherwise. |
ImportIssue
One problem found in the file, or one thing worth knowing.
| Field | Type | Required | Description |
|---|---|---|---|
line |
integer or null | yes | The row in the file (the header is row 1); null for a problem with the file as a whole or with the entry built from all rows. |
column |
string or null | yes | The column of the template the problem is in; null when it is about the row or the file as a whole. |
message |
string | yes | What is wrong, and how to fix it. |
ImportSummary
What the import created (or, in a dry run, would create).
| Field | Type | Required | Description |
|---|---|---|---|
created |
integer | yes | Records created: customers, vendors, accounts, or documents (open items); 1 for the opening entry. |
waiting_for_activation |
integer or null | Vendors (and customers, if the approval policy says so) created as drafts that a second person must activate (GET /v1/approvals) before their documents can be posted or paid; null for other imports. |
|
posted |
integer or null | Open items posted (their opening entries are in the ledger); null for other imports. | |
waiting_for_approval |
integer or null | Entries the approval policy sent to a second person instead of posting; null for other imports. | |
total_debit |
string or null | opening_balances: the debits of the file in the entity's currency. |
|
total_credit |
string or null | opening_balances: the credits of the file in the entity's currency. |
|
clearing_difference |
string or null | opening_balances: debits less credits of the file, which the entry books to the opening balance clearing account (the receivables and payables of the trial balance are imported as open items, so they are not in the file's entry); 0 when the file balances by itself. |
|
amounts_by_currency |
map of string or null | Open items: the total imported per currency (credit notes count negative). | |
clearing_balance |
string or null | The balance of the entity's opening balance clearing account after the import (debits less credits), in its functional currency. After the opening balances and both open item lists are imported it is 0 when they agree. | |
entry |
ImportEntry or null |
opening_balances: the opening entry; null otherwise. |
SalesforceImportResult
| Field | Type | Required | Description |
|---|---|---|---|
object |
"accounts" | "contacts" | "events" | "notes" | "opportunities" | "price_book_entries" | "price_books" | "products" | "tasks" | "users" |
yes | What the file holds. |
mode |
"commit" | "dry_run" |
yes | dry_run (nothing is saved) or commit (all or nothing). |
entity |
string | yes | Code of the entity the import is for. |
format |
"csv" | "xlsx" |
yes | csv or xlsx, as detected from the file. |
filename |
string | yes | The uploaded file's name. |
sha256 |
string | yes | SHA-256 of the file, kept with what it created. |
rows |
integer | yes | Data rows in the file (empty rows are skipped). |
ok |
boolean | yes | True when the file has no error: a dry run says a commit would succeed; a commit has succeeded. |
committed |
boolean | yes | True only for a commit that saved the records. |
columns |
array of string | yes | The file's columns, as the header names them. |
mapping |
array of FieldMapping |
yes | Each of our fields and the column it is read from: found by its Salesforce name (API name or label), or as mapping named it. |
unused_columns |
array of string | yes | Columns of the file no field is read from (Salesforce exports every field; most are not needed). |
errors |
array of ImportIssue |
yes | Every problem found (all of them): a dry run lists them with 200, a commit refuses with 422 and the same list. |
warnings |
array of ImportIssue |
yes | Things worth knowing that do not stop the import: a row skipped, a value shortened, a code made up because the file has none. |
summary |
SalesforceSummary |
yes | What was (or would be) created. |
SalesforceSummary
What the import created (or, in a dry run, would create).
| Field | Type | Required | Description |
|---|---|---|---|
created |
integer | yes | Rows imported now: each became a record. |
already_imported |
integer | yes | Rows whose Salesforce id an earlier import already brought over: left as they are, nothing is created twice. |
skipped |
integer | yes | Rows not imported, each with a note saying why (deleted in Salesforce, about a record that was not imported, ...). |
records |
map of integer | yes | What was created, by kind: customers, deals, tasks, notes, products, prices, price_lists, people_linked, ... |