Factuarea API

Export and import

Export invoices to an Excel/CSV spreadsheet (SUMMARY or ITEMS, capped at 5000) and import clients from a CSV with a dry-run preview, column mapping, a downloadable template and partial-success.

Two file-based operations move data in and out of Factuarea over the public API: export invoices to a spreadsheet, and import clients from a CSV. Both reuse the same engines the dashboard uses, and the import follows the partial-success contract — one bad row never sinks the whole file.

Export invoices to a spreadsheet

POST /v1/invoices/export/excel (scope invoices:read) builds an XLSX or CSV spreadsheet of your invoices and streams the binary file back. It is a read operation: nothing is created or modified.

Two orthogonal axes control the output:

ParameterValuesMeaning
formatSUMMARY (default) · ITEMSContent layout. SUMMARY is one row per invoice; ITEMS is one row per invoice line (header columns repeated on each line).
file_formatxlsx (default) · csvFile container.

Pick the invoices to export in either of two ways:

  • By id — pass invoice_ids with the UUID v7 ids of specific invoices.
  • By filter — omit invoice_ids and narrow the set with status, date_from, date_to, client_id, series_id and search. date_from and date_to filter the issue date (inclusive); client_id and series_id take the public UUID v7 of the client/series.
curl -s -X POST https://api.factuarea.com/v1/invoices/export/excel \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "ITEMS",
    "file_format": "xlsx",
    "status": "paid",
    "date_from": "2026-01-01",
    "date_to": "2026-03-31"
  }' \
  -o invoices-q1.xlsx

The 5000-invoice limit

The selected set is capped at 5000 invoices. If your filters match more than that, the API does not truncate silently — it returns 422 with the export_limit_exceeded error code:

{
  "error": {
    "type": "invalid_request_error",
    "code": "export_limit_exceeded",
    "message": "La exportación supera el máximo de 5000 facturas."
  }
}

Narrow the date range, the status or the client, or split the export into several calls, so each request stays under the limit.

client_id, series_id and the invoice_ids entries are matched inside your company. A UUID that does not exist or belongs to another company is simply dropped from the selection — it never leaks cross-tenant data and never returns a global 404.

Import clients from a CSV

POST /v1/clients/import (scope clients:write) reads a delimited file and creates one client per valid row. The request is multipart/form-data — it carries a file, not a JSON body — with three fields:

FieldTypeMeaning
filefileThe CSV/XLSX/XLS/ODS/TXT file, up to 10 MB.
mappingobject{ "csv_header": "target_field" }. Must map at least name and tax_id.
dry_runbooleanWhen true, validate and preview without creating anything. Default false.

The mapping tells the importer which spreadsheet column feeds which client field. The target set must include name and tax_id — without them a client cannot be created, and the request is rejected with 422 before any row is processed.

Download the template

GET /v1/clients/import/template streams a ready-to-fill CSV (UTF-8 with BOM so Excel opens it cleanly) whose header row lists every column the importer understands: Nombre, NIF/CIF, Razón social, Email, Teléfono, address fields, default VAT/retention, IBAN and more. Two example rows show the expected format.

curl -s https://api.factuarea.com/v1/clients/import/template \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -o clients-template.csv

Dry run first, then import

Always validate with dry_run=true before you commit. The preview returns a per-row report and writes nothing:

curl -s -X POST https://api.factuarea.com/v1/clients/import \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -F "file=@clients.csv" \
  -F 'mapping={"Nombre":"name","NIF/CIF":"tax_id","Email":"email"};type=application/json' \
  -F "dry_run=true"
{
  "data": {
    "object": "client_import_preview",
    "total_rows": 3,
    "rows": [
      { "row": 2, "status": "valid", "errors": [], "warnings": [] },
      {
        "row": 3,
        "status": "error",
        "errors": [{ "param": "tax_id", "code": "invalid_tax_id", "message": "El NIF no es válido." }],
        "warnings": []
      },
      { "row": 4, "status": "valid", "errors": [], "warnings": [] }
    ]
  }
}

row is the 1-based line number in the file (the header is row 1, so the first data row is row 2). status is valid or error; each errors[] item carries the offending param, a stable code and a Spanish message.

When the preview is clean, resend the same file and mapping with dry_run=false (or omit it). Only valid rows are created; rejected rows come back in failures[], and the response follows the partial-success shape with a per-row results[]:

{
  "data": {
    "total": 3,
    "successful": 2,
    "failed": 1,
    "failures": [
      {
        "index": 1,
        "error_code": "invalid_tax_id",
        "error_message": "El NIF no es válido.",
        "errors": [{ "param": "tax_id", "code": "invalid_tax_id", "message": "El NIF no es válido." }],
        "warnings": []
      }
    ],
    "results": [
      { "row": 3, "status": "error", "errors": [{ "param": "tax_id", "code": "invalid_tax_id", "message": "El NIF no es válido." }], "warnings": [] }
    ]
  }
}

total === successful + failed always holds. A duplicate row (a client already on file, by the dedup rule) is skipped, not failed — it counts towards successful and is not created again, so re-running the same file is safe.

Branch on error_code / code, never on the message — the message is Spanish, human-facing text. The per-row codes come from the v1 error catalog.

File-size limit

The v1 import is synchronous so it can return the per-row result in the same response. Files are capped at fewer than 200 rows; a larger file is rejected with 422 and the client_import_too_large code. Split a big list into batches under that limit and import them in sequence.

The file (10 MB) and dry_run rejections, and the 5000/200 caps, are enforced before any row is written. The dry-run preview is the cheapest way to catch malformed rows — use it before every real import.

On this page