Factuarea APIDevelopers
Contract

Import results, task import retries and new read fields

Contact imports report updated rows, warnings and why each row was skipped; task imports retry safely; invoices expose origin; quotes and proformas expose operation_regime.

5 October 2026

This release changes the contract of the import operations and adds read fields to invoices, quotes and proformas. No operation is new: every one listed below already existed and its response or behavior was updated. The list of operations is at the end of the page.

Contact import tracking

GET /v1/contacts/imports/{id}, POST /v1/contacts/import and POST /v1/contacts/import/preview (and the MCP tools get_contact_import, import_contacts and preview_contacts_import) report more about what an import did.

FieldMeaning
updated_countRows that updated an existing contact. added_count no longer includes them: total_rows = added_count + updated_count + skipped_count + failed_count + unprocessed_count.
warnings_countWarnings produced by the import. It is not part of that sum.
outcome_kindchanges_applied, nothing_changed, partial or failed. completed does not mean something changed: nothing_changed is an import that finished without creating or updating anything, so do not present it as a success. partial has applied and failed rows; failed has failed rows and none applied.
skip_reasonsSkipped rows counted by reason.
first_diagnosticsUp to 20 grouped diagnostics (code, field, severity, message, count, first_row).
outcomes_truncatedtrue when outcomes holds the first 500 rows and the import has more.
reason (per row)Why a row was skipped, failed or left the contact unchanged (already_exists, no_changes, merge_review_required, update_not_allowed, identity_conflict, validation_failed…); null when the row was applied.

The preview and the synchronous import response add the same updated_count, warnings_count, skip_reasons and per-row reason, plus the skip action and counter: rows that change nothing because they are identical to the existing contact (no_changes) or target an archived contact (update_not_allowed). The preview also reports merge_candidate rows (see merge below).

The synchronous response of POST /v1/contacts/import (and the import_contacts tool) also carries outcome_kind, with the same values as the tracking resource. It is null in the preview, in dry_run and in queued imports (202), and takes its value when the execution finishes.

What conflict_strategy does

conflict_strategy decides what happens when a row matches an existing contact by tax_id or external_id:

ValueBehavior
reject (default)Any match is a conflict, except when the row only adds a role the contact does not have yet.
updateApplies the fields that carry a value. An empty cell never clears a field.
mergeApplies nothing and flags the row for manual review: result=skipped, reason=merge_review_required.

An import never changes the tax_id or the external_id of an existing contact and never reactivates an archived one.

Mapping and columns

A mapping that points to a column the file does not have is now rejected with 422 mapping_header_not_found with param: mapping. Read the headers again and send a mapping whose keys match them exactly. The code is in the error catalog.

The importer accepts four new columns: iban and bic (a bank account added to the contact's existing ones: collection for a customer, payment for a supplier), customer_default_price_list_name (name of an existing price list, instead of its UUID) and supplier_default_tax_code (code of an existing tax, instead of its UUID).

A negative retention percentage (customer_retention_rate, supplier_retention_rate; the legacy customer convention, for example -15) is stored as a positive value (15), and the row gets the RETENTION_SIGN_NORMALIZED warning in its diagnostics.

Import template

GET /v1/contacts/import/template now takes format=csv|xlsx. csv is the default and returns a UTF-8 file with a BOM and only the headers, separated by semicolons. xlsx returns a workbook with the Data, Instructions, Examples and Allowed values sheets. The response is an attachment (plantilla-contactos.csv or plantilla-contactos.xlsx, with Content-Disposition). It still requires contacts:read and contains no company data.

Task import retries

POST /v1/projects/{project}/tasks/import and the MCP tool import_project_tasks can now be retried without duplicating tasks:

  • Repeating the call with the same key and the same document returns the report of the first call and creates nothing.
  • Reusing the key with a different document answers 409 idempotency_key_reused.
  • A new key, or no key in MCP, creates a new copy of the tasks. In MCP the idempotency_key argument is optional; over HTTP the Idempotency-Key header stays required.

The report adds warnings_total, the number of warnings the import produced, and warnings_truncated, true when some were left out. Only the first 200 warnings are kept in warnings.

New read fields

ResourceFieldMeaning
Invoice (v1 and MCP)originnative for an invoice issued in Factuarea, historical_import for a historical invoice registered by import: literal number from the file, no series counter and no VeriFactu record.
QuotereferenceOrigin number of the document, for example the one it had in a previous system when imported. null if not set.
Quote and proformaoperation_regimeVAT operation regime: general, intracomunitaria, importacion_exportacion or isp.

The reason of a stock movement also admits initial.

Updated endpoints48

EndpointDescription
POST/v1/contacts/import/previewPreview a contact import
POST/v1/contacts/importImport contacts
GET/v1/contacts/imports/{id}Retrieve a contact import
GET/v1/contacts/import/templateDownload the contact import template
POST/v1/projects/{project}/tasks/importImport tasks into a project
POST/v1/invoices/{invoice}/annulAnnul an invoice
POST/v1/invoices/{invoice}/assign-real-numberAssign a real invoice number
POST/v1/invoices/{invoice}/correctiveGenerate corrective invoice
POST/v1/invoicesCreate an invoice
GET/v1/invoicesList all invoices
GET/v1/invoices/{invoice}Retrieve an invoice
PUT/v1/invoices/{invoice}Update an invoice
POST/v1/invoices/{invoice}/duplicateDuplicate an invoice
POST/v1/invoices/find-by-external-idFind an invoice by external ID
POST/v1/invoices/find-by-numberFind an invoice by number
POST/v1/invoices/{invoice}/issueIssue an invoice
GET/v1/invoices/{invoice}/correctivesList corrective invoices
POST/v1/invoices/{invoice}/mark-paidMark invoice as paid
POST/v1/invoices/{invoice}/mark-sentMark an invoice as sent
PATCH/v1/invoices/{invoice}/rescheduleReschedule an invoice
POST/v1/invoices/{invoice}/scheduleSchedule an invoice
POST/v1/invoices/{invoice}/sendSend invoice by email
POST/v1/invoices/substitute-simplifiedSubstitute simplified invoices with full invoice
POST/v1/invoices/{invoice}/unscheduleUnschedule an invoice
POST/v1/invoices/{invoice}/unsendUnsend an invoice
POST/v1/invoices/{invoice}/voidVoid an invoice
POST/v1/delivery_notes/{delivery_note}/convertConvert delivery note to invoice
POST/v1/proformas/{proforma}/convertConvert proforma to invoice
POST/v1/quotes/{quote}/convertConvert quote to invoice
POST/v1/quotes/{quote}/acceptAccept a quote
POST/v1/quotes/{quote}/rejectReject a quote
POST/v1/quotesCreate a quote
GET/v1/quotesList all quotes
GET/v1/quotes/{quote}Retrieve a quote
PUT/v1/quotes/{quote}Update a quote
POST/v1/quotes/{quote}/duplicateDuplicate a quote
POST/v1/quotes/find-by-external-idFind a quote by external ID
POST/v1/quotes/{quote}/sendSend quote by email
POST/v1/proformas/{proforma}/acceptAccept a proforma
POST/v1/proformas/{proforma}/rejectReject a proforma
POST/v1/proformasCreate a proforma
GET/v1/proformasList all proformas
GET/v1/proformas/{proforma}Retrieve a proforma
PUT/v1/proformas/{proforma}Update a proforma
POST/v1/proformas/{proforma}/duplicateDuplicate a proforma
POST/v1/proformas/find-by-external-idFind a proforma by external ID
POST/v1/proformas/{proforma}/sendSend proforma by email
GET/v1/products/{product}/stock-movementsList stock movements of a product

On this page

Need a hand?Contact support